Added Jira connectivity testing. Added alert management in AdminForm
This commit is contained in:
@@ -66,4 +66,10 @@ def post(session, _id: str, args: dict):
|
|||||||
def post(session, _id: str):
|
def post(session, _id: str):
|
||||||
logger.debug(f"Entering {Routes.ConfigureJiraCancel} with args {debug_session(session)}, {_id=}")
|
logger.debug(f"Entering {Routes.ConfigureJiraCancel} with args {debug_session(session)}, {_id=}")
|
||||||
instance = InstanceManager.get(session, _id)
|
instance = InstanceManager.get(session, _id)
|
||||||
return instance.cancel_jira_settings()
|
return instance.cancel_jira_settings()
|
||||||
|
|
||||||
|
@rt(Routes.ConfigureJiraTest)
|
||||||
|
def post(session, _id: str, args: dict):
|
||||||
|
logger.debug(f"Entering {Routes.ConfigureJiraTest} with args {debug_session(session)}, {_id=}, {args=}")
|
||||||
|
instance = InstanceManager.get(session, _id)
|
||||||
|
return instance.test_jira_settings(args)
|
||||||
@@ -1,10 +1,31 @@
|
|||||||
from fastcore.basics import NotStr
|
from fastcore.basics import NotStr
|
||||||
|
|
||||||
icon_jira = NotStr("""<svg name="jara" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
icon_jira = NotStr("""<svg name="jira" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<defs>
|
||||||
<style>.a{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style>
|
<style>.a{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style>
|
||||||
</defs>
|
</defs>
|
||||||
<path class="a" d="M5.5,22.9722h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556A8.7361,8.7361,0,0,0,25.0278,42.5h0V22.9722Z"/>
|
<path class="a" d="M5.5,22.9722h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556A8.7361,8.7361,0,0,0,25.0278,42.5h0V22.9722Z"/>
|
||||||
<path class="a" d="M14.2361,14.2361h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556a8.7361,8.7361,0,0,0,8.7361,8.7361h0V14.2361Z"/>
|
<path class="a" d="M14.2361,14.2361h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556a8.7361,8.7361,0,0,0,8.7361,8.7361h0V14.2361Z"/>
|
||||||
<path class="a" d="M22.9722,5.5h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556A8.7361,8.7361,0,0,0,42.5,25.0278h0V5.5Z"/>
|
<path class="a" d="M22.9722,5.5h0a8.7361,8.7361,0,0,0,8.7361,8.7361h2.0556v2.0556A8.7361,8.7361,0,0,0,42.5,25.0278h0V5.5Z"/>
|
||||||
</svg>""")
|
</svg>""")
|
||||||
|
|
||||||
|
|
||||||
|
icon_msg_info = NotStr("""<svg name="info" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
""")
|
||||||
|
|
||||||
|
icon_msg_success = NotStr("""<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
""")
|
||||||
|
|
||||||
|
icon_msg_warning = NotStr("""<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
""")
|
||||||
|
|
||||||
|
icon_msg_error = NotStr("""<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
""")
|
||||||
@@ -63,6 +63,14 @@ class AdminCommandManager(BaseCommandManager):
|
|||||||
"hx-swap": "outerHTML",
|
"hx-swap": "outerHTML",
|
||||||
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_jira(self):
|
||||||
|
return {
|
||||||
|
"hx-post": f"{ROUTE_ROOT}{Routes.ConfigureJiraTest}",
|
||||||
|
"hx-target": f"#{self._owner.tabs_manager.get_id()}",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
"hx-vals": f'js:{{"_id": "{self._id}"}}',
|
||||||
|
}
|
||||||
|
|
||||||
class ImportHolidaysCommandManager(BaseCommandManager):
|
class ImportHolidaysCommandManager(BaseCommandManager):
|
||||||
def __init__(self, owner):
|
def __init__(self, owner):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from components.BaseComponent import BaseComponent
|
|||||||
from components.admin.admin_db_manager import AdminDbManager
|
from components.admin.admin_db_manager import AdminDbManager
|
||||||
from components.admin.assets.icons import icon_jira
|
from components.admin.assets.icons import icon_jira
|
||||||
from components.admin.commands import AdminCommandManager
|
from components.admin.commands import AdminCommandManager
|
||||||
from components.admin.components.AdminForm import AdminFormItem, AdminFormType, AdminForm
|
from components.admin.components.AdminForm import AdminFormItem, AdminFormType, AdminForm, AdminButton, AdminMessageType
|
||||||
from components.admin.components.ImportHolidays import ImportHolidays
|
from components.admin.components.ImportHolidays import ImportHolidays
|
||||||
from components.admin.constants import ADMIN_INSTANCE_ID, ADMIN_AI_BUDDY_INSTANCE_ID, ADMIN_JIRA_INSTANCE_ID
|
from components.admin.constants import ADMIN_INSTANCE_ID, ADMIN_AI_BUDDY_INSTANCE_ID, ADMIN_JIRA_INSTANCE_ID
|
||||||
from components.aibuddy.assets.icons import icon_brain_ok
|
from components.aibuddy.assets.icons import icon_brain_ok
|
||||||
@@ -14,6 +14,7 @@ from components.hoildays.assets.icons import icon_holidays
|
|||||||
from components.tabs.components.MyTabs import MyTabs
|
from components.tabs.components.MyTabs import MyTabs
|
||||||
from components_helpers import mk_ellipsis, mk_icon
|
from components_helpers import mk_ellipsis, mk_icon
|
||||||
from core.instance_manager import InstanceManager
|
from core.instance_manager import InstanceManager
|
||||||
|
from core.jira import Jira
|
||||||
|
|
||||||
|
|
||||||
class Admin(BaseComponent):
|
class Admin(BaseComponent):
|
||||||
@@ -36,7 +37,7 @@ class Admin(BaseComponent):
|
|||||||
hooks = {
|
hooks = {
|
||||||
"on_ok": self.commands.save_ai_buddy(),
|
"on_ok": self.commands.save_ai_buddy(),
|
||||||
"on_cancel": self.commands.cancel_ai_buddy(),
|
"on_cancel": self.commands.cancel_ai_buddy(),
|
||||||
"ok_title": "Apply"
|
"ok_title": "Apply",
|
||||||
}
|
}
|
||||||
form = InstanceManager.get(self._session,
|
form = InstanceManager.get(self._session,
|
||||||
AdminForm.create_component_id(self._session, prefix=self._id),
|
AdminForm.create_component_id(self._session, prefix=self._id),
|
||||||
@@ -68,7 +69,8 @@ class Admin(BaseComponent):
|
|||||||
hooks = {
|
hooks = {
|
||||||
"on_ok": self.commands.save_configure_jira(),
|
"on_ok": self.commands.save_configure_jira(),
|
||||||
"on_cancel": self.commands.cancel_configure_jira(),
|
"on_cancel": self.commands.cancel_configure_jira(),
|
||||||
"ok_title": "Apply"
|
"ok_title": "Apply",
|
||||||
|
"extra_buttons": [AdminButton("Test", self.commands.test_jira)]
|
||||||
}
|
}
|
||||||
|
|
||||||
form = InstanceManager.get(self._session,
|
form = InstanceManager.get(self._session,
|
||||||
@@ -85,7 +87,7 @@ class Admin(BaseComponent):
|
|||||||
return self._add_tab(ADMIN_JIRA_INSTANCE_ID, "Admin - Jira Configuration", form)
|
return self._add_tab(ADMIN_JIRA_INSTANCE_ID, "Admin - Jira Configuration", form)
|
||||||
|
|
||||||
def update_ai_buddy_settings(self, values: dict):
|
def update_ai_buddy_settings(self, values: dict):
|
||||||
values = self.manage_lists(values)
|
values = AdminForm.get_fields_values(values)
|
||||||
self.db.ai_buddy.update(values, ignore_missing=True)
|
self.db.ai_buddy.update(values, ignore_missing=True)
|
||||||
return self.tabs_manager.render()
|
return self.tabs_manager.render()
|
||||||
|
|
||||||
@@ -95,15 +97,25 @@ class Admin(BaseComponent):
|
|||||||
return self.tabs_manager.render()
|
return self.tabs_manager.render()
|
||||||
|
|
||||||
def update_jira_settings(self, values: dict):
|
def update_jira_settings(self, values: dict):
|
||||||
values = self.manage_lists(values)
|
values = AdminForm.get_fields_values(values)
|
||||||
self.db.jira.update(values, ignore_missing=True)
|
self.db.jira.update(values, ignore_missing=True)
|
||||||
return self.tabs_manager.render()
|
return self.tabs_manager.render()
|
||||||
|
|
||||||
def cancel_jira_settings(self):
|
def cancel_jira_settings(self):
|
||||||
tab_id = self.tabs_manager.get_tab_id(ADMIN_JIRA_INSTANCE_ID)
|
tab_id = self.tabs_manager.get_tab_id(ADMIN_JIRA_INSTANCE_ID)
|
||||||
self.tabs_manager.remove_tab(tab_id)
|
self.tabs_manager.remove_tab(tab_id)
|
||||||
return self.tabs_manager.render()
|
return self.tabs_manager.render()
|
||||||
|
|
||||||
|
def test_jira_settings(self, values: dict):
|
||||||
|
values = AdminForm.get_fields_values(values)
|
||||||
|
jira = Jira(values["user_name"], values["api_token"])
|
||||||
|
form = self.tabs_manager.get_tab_content_by_key(ADMIN_JIRA_INSTANCE_ID)
|
||||||
|
res = jira.test()
|
||||||
|
if res.status_code == 200:
|
||||||
|
form.set_message("Success !", AdminMessageType.SUCCESS)
|
||||||
|
else:
|
||||||
|
form.set_message(f"Error {res.status_code} - {res.text}", AdminMessageType.ERROR)
|
||||||
|
return self.tabs_manager.render()
|
||||||
|
|
||||||
def __ft__(self):
|
def __ft__(self):
|
||||||
return Div(
|
return Div(
|
||||||
@@ -138,40 +150,3 @@ class Admin(BaseComponent):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def create_component_id(session):
|
def create_component_id(session):
|
||||||
return f"{ADMIN_INSTANCE_ID}{session['user_id']}"
|
return f"{ADMIN_INSTANCE_ID}{session['user_id']}"
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def manage_lists(data_dict):
|
|
||||||
"""
|
|
||||||
Processes a dictionary of key-value pairs to reorganize keys based on specific
|
|
||||||
criteria. If a key ends with its corresponding string value, the method extracts
|
|
||||||
the prefix of the key (the portion of the key before the value) and groups the
|
|
||||||
value under this prefix in a list. Otherwise, the original key-value pair is
|
|
||||||
preserved in the resulting dictionary.
|
|
||||||
|
|
||||||
:param data_dict: Dictionary where each key is a string and its corresponding
|
|
||||||
value can be of any type.
|
|
||||||
:type data_dict: dict
|
|
||||||
:return: A dictionary where the keys have been categorized into groups
|
|
||||||
based on whether they end with the same string value, reorganized into
|
|
||||||
lists, while preserving other key-value pairs as they are.
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
|
|
||||||
result_dict = {}
|
|
||||||
|
|
||||||
for key, value in data_dict.items():
|
|
||||||
# Check if the value is a string and the key ends with the value
|
|
||||||
if isinstance(value, str) and key.endswith(value):
|
|
||||||
# Find the beginning part of the key (before the value)
|
|
||||||
prefix = key.replace(value, '').rstrip('_')
|
|
||||||
|
|
||||||
# Add the value to the list under the prefix key
|
|
||||||
if prefix not in result_dict:
|
|
||||||
result_dict[prefix] = []
|
|
||||||
|
|
||||||
result_dict[prefix].append(value)
|
|
||||||
|
|
||||||
else:
|
|
||||||
result_dict[key] = value
|
|
||||||
|
|
||||||
return result_dict
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any, Callable
|
||||||
|
|
||||||
from fasthtml.components import *
|
from fasthtml.components import *
|
||||||
|
|
||||||
|
from assets.icons import icon_error
|
||||||
from components.BaseComponent import BaseComponent
|
from components.BaseComponent import BaseComponent
|
||||||
from components_helpers import apply_boundaries, mk_dialog_buttons, safe_get_dialog_buttons_parameters
|
from components.admin.assets.icons import icon_msg_success, icon_msg_info, icon_msg_error, icon_msg_warning
|
||||||
|
from components_helpers import apply_boundaries, mk_dialog_buttons, safe_get_dialog_buttons_parameters, mk_icon
|
||||||
from core.utils import get_unique_id
|
from core.utils import get_unique_id
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +20,14 @@ class AdminFormType:
|
|||||||
TEXTAREA = "textarea"
|
TEXTAREA = "textarea"
|
||||||
|
|
||||||
|
|
||||||
|
class AdminMessageType:
|
||||||
|
NONE = "none"
|
||||||
|
SUCCESS = "success"
|
||||||
|
ERROR = "error"
|
||||||
|
INFO = "info"
|
||||||
|
WARNING = "warning"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AdminFormItem:
|
class AdminFormItem:
|
||||||
name: str
|
name: str
|
||||||
@@ -27,6 +37,12 @@ class AdminFormItem:
|
|||||||
possible_values: list[str] = None
|
possible_values: list[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AdminButton:
|
||||||
|
title: str
|
||||||
|
on_click: Callable = None
|
||||||
|
|
||||||
|
|
||||||
class AdminForm(BaseComponent):
|
class AdminForm(BaseComponent):
|
||||||
def __init__(self, session, _id, owner, title: str, obj: Any, form_fields: list[AdminFormItem], hooks=None, key=None,
|
def __init__(self, session, _id, owner, title: str, obj: Any, form_fields: list[AdminFormItem], hooks=None, key=None,
|
||||||
boundaries=None):
|
boundaries=None):
|
||||||
@@ -38,6 +54,21 @@ class AdminForm(BaseComponent):
|
|||||||
self.title = title
|
self.title = title
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
self.form_fields = form_fields
|
self.form_fields = form_fields
|
||||||
|
self.message = None
|
||||||
|
|
||||||
|
def set_message(self, message, msg_type: AdminMessageType.NONE):
|
||||||
|
if msg_type == AdminMessageType.NONE:
|
||||||
|
self.message = message
|
||||||
|
else:
|
||||||
|
if msg_type == AdminMessageType.SUCCESS:
|
||||||
|
icon = icon_msg_success
|
||||||
|
elif msg_type == AdminMessageType.ERROR:
|
||||||
|
icon = icon_msg_error
|
||||||
|
elif msg_type == AdminMessageType.WARNING:
|
||||||
|
icon = icon_msg_warning
|
||||||
|
else:
|
||||||
|
icon = icon_msg_info
|
||||||
|
self.message = Div(icon, Span(message), role=msg_type, cls=f"alert alert-{msg_type} mr-2")
|
||||||
|
|
||||||
def mk_input(self, item: AdminFormItem):
|
def mk_input(self, item: AdminFormItem):
|
||||||
return Input(
|
return Input(
|
||||||
@@ -62,7 +93,7 @@ class AdminForm(BaseComponent):
|
|||||||
cls="checkbox checkbox-xs",
|
cls="checkbox checkbox-xs",
|
||||||
checked=value in current_values
|
checked=value in current_values
|
||||||
),
|
),
|
||||||
|
|
||||||
cls="checkbox-item") for value in item.possible_values]
|
cls="checkbox-item") for value in item.possible_values]
|
||||||
|
|
||||||
return Div(*checkbox_items, cls="adm-items-group")
|
return Div(*checkbox_items, cls="adm-items-group")
|
||||||
@@ -95,9 +126,20 @@ class AdminForm(BaseComponent):
|
|||||||
else:
|
else:
|
||||||
return self.mk_input(item)
|
return self.mk_input(item)
|
||||||
|
|
||||||
|
def mk_extra_buttons(self):
|
||||||
|
extra_buttons = self._hooks.get("extra_buttons", None)
|
||||||
|
if not extra_buttons:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return Div(
|
||||||
|
*[Button(btn.title, cls="btn btn-ghost btn-sm", **btn.on_click()) for btn in extra_buttons],
|
||||||
|
cls="flex justify-end"
|
||||||
|
)
|
||||||
|
|
||||||
def __ft__(self):
|
def __ft__(self):
|
||||||
return Form(
|
return Form(
|
||||||
Fieldset(Legend(self.title, cls="fieldset-legend"),
|
Fieldset(Legend(self.title, cls="fieldset-legend"),
|
||||||
|
Div(self.message),
|
||||||
*[
|
*[
|
||||||
Div(
|
Div(
|
||||||
Label(item.title, cls="label"),
|
Label(item.title, cls="label"),
|
||||||
@@ -107,6 +149,7 @@ class AdminForm(BaseComponent):
|
|||||||
|
|
||||||
for item in self.form_fields
|
for item in self.form_fields
|
||||||
],
|
],
|
||||||
|
self.mk_extra_buttons(),
|
||||||
mk_dialog_buttons(**safe_get_dialog_buttons_parameters(self._hooks)),
|
mk_dialog_buttons(**safe_get_dialog_buttons_parameters(self._hooks)),
|
||||||
**apply_boundaries(self._boundaries),
|
**apply_boundaries(self._boundaries),
|
||||||
cls="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4"
|
cls="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4"
|
||||||
@@ -119,3 +162,40 @@ class AdminForm(BaseComponent):
|
|||||||
suffix = get_unique_id()
|
suffix = get_unique_id()
|
||||||
|
|
||||||
return f"{prefix}{suffix}"
|
return f"{prefix}{suffix}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_fields_values(data_dict):
|
||||||
|
"""
|
||||||
|
Processes a dictionary of key-value pairs to reorganize keys based on specific
|
||||||
|
criteria. If a key ends with its corresponding string value, the method extracts
|
||||||
|
the prefix of the key (the portion of the key before the value) and groups the
|
||||||
|
value under this prefix in a list. Otherwise, the original key-value pair is
|
||||||
|
preserved in the resulting dictionary.
|
||||||
|
|
||||||
|
:param data_dict: Dictionary where each key is a string and its corresponding
|
||||||
|
value can be of any type.
|
||||||
|
:type data_dict: dict
|
||||||
|
:return: A dictionary where the keys have been categorized into groups
|
||||||
|
based on whether they end with the same string value, reorganized into
|
||||||
|
lists, while preserving other key-value pairs as they are.
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
result_dict = {}
|
||||||
|
|
||||||
|
for key, value in data_dict.items():
|
||||||
|
# Check if the value is a string and the key ends with the value
|
||||||
|
if isinstance(value, str) and key.endswith(value):
|
||||||
|
# Find the beginning part of the key (before the value)
|
||||||
|
prefix = key.replace(value, '').rstrip('_')
|
||||||
|
|
||||||
|
# Add the value to the list under the prefix key
|
||||||
|
if prefix not in result_dict:
|
||||||
|
result_dict[prefix] = []
|
||||||
|
|
||||||
|
result_dict[prefix].append(value)
|
||||||
|
|
||||||
|
else:
|
||||||
|
result_dict[key] = value
|
||||||
|
|
||||||
|
return result_dict
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ class Routes:
|
|||||||
PasteHolidays = "/paste-holidays"
|
PasteHolidays = "/paste-holidays"
|
||||||
ConfigureJira = "/configure-jira"
|
ConfigureJira = "/configure-jira"
|
||||||
ConfigureJiraCancel = "/configure-jira-cancel"
|
ConfigureJiraCancel = "/configure-jira-cancel"
|
||||||
|
ConfigureJiraTest = "/configure-jira-test"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def post(session, _id: str, name: str, tab_boundaries: str):
|
|||||||
|
|
||||||
|
|
||||||
@rt(Routes.AddComponent)
|
@rt(Routes.AddComponent)
|
||||||
def post(session, _id: str, component_type: str, x: int, y: int):
|
def post(session, _id: str, component_type: str, x: float, y: float):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Entering {Routes.AddComponent} with args {debug_session(session)}, {_id=}, {component_type=}, {x=}, {y=}")
|
f"Entering {Routes.AddComponent} with args {debug_session(session)}, {_id=}, {component_type=}, {x=}, {y=}")
|
||||||
instance = InstanceManager.get(session, _id)
|
instance = InstanceManager.get(session, _id)
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ class Jira:
|
|||||||
self.api_token = api_token
|
self.api_token = api_token
|
||||||
self.auth = HTTPBasicAuth(self.user_name, self.api_token)
|
self.auth = HTTPBasicAuth(self.user_name, self.api_token)
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
url = f"{JIRA_ROOT}/myself"
|
||||||
|
|
||||||
|
response = requests.request(
|
||||||
|
"GET",
|
||||||
|
url,
|
||||||
|
headers=DEFAULT_HEADERS,
|
||||||
|
auth=self.auth
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
def issue(self, issue_id: str) -> Expando:
|
def issue(self, issue_id: str) -> Expando:
|
||||||
"""
|
"""
|
||||||
Retrieve an issue
|
Retrieve an issue
|
||||||
|
|||||||
Reference in New Issue
Block a user