ghnova 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ghnova/__init__.py +8 -0
- ghnova/__main__.py +8 -0
- ghnova/cli/__init__.py +1 -0
- ghnova/cli/config/__init__.py +1 -0
- ghnova/cli/config/add.py +48 -0
- ghnova/cli/config/delete.py +50 -0
- ghnova/cli/config/list.py +40 -0
- ghnova/cli/config/main.py +27 -0
- ghnova/cli/config/update.py +59 -0
- ghnova/cli/issue/__init__.py +7 -0
- ghnova/cli/issue/create.py +155 -0
- ghnova/cli/issue/get.py +119 -0
- ghnova/cli/issue/list.py +267 -0
- ghnova/cli/issue/lock.py +110 -0
- ghnova/cli/issue/main.py +31 -0
- ghnova/cli/issue/unlock.py +101 -0
- ghnova/cli/issue/update.py +164 -0
- ghnova/cli/main.py +117 -0
- ghnova/cli/repository/__init__.py +1 -0
- ghnova/cli/repository/list.py +201 -0
- ghnova/cli/repository/main.py +21 -0
- ghnova/cli/user/__init__.py +1 -0
- ghnova/cli/user/ctx_info.py +105 -0
- ghnova/cli/user/get.py +98 -0
- ghnova/cli/user/list.py +78 -0
- ghnova/cli/user/main.py +27 -0
- ghnova/cli/user/update.py +164 -0
- ghnova/cli/utils/__init__.py +7 -0
- ghnova/cli/utils/auth.py +67 -0
- ghnova/client/__init__.py +8 -0
- ghnova/client/async_github.py +121 -0
- ghnova/client/base.py +78 -0
- ghnova/client/github.py +107 -0
- ghnova/config/__init__.py +8 -0
- ghnova/config/manager.py +209 -0
- ghnova/config/model.py +58 -0
- ghnova/issue/__init__.py +8 -0
- ghnova/issue/async_issue.py +554 -0
- ghnova/issue/base.py +469 -0
- ghnova/issue/issue.py +584 -0
- ghnova/repository/__init__.py +8 -0
- ghnova/repository/async_repository.py +134 -0
- ghnova/repository/base.py +124 -0
- ghnova/repository/repository.py +134 -0
- ghnova/resource/__init__.py +8 -0
- ghnova/resource/async_resource.py +88 -0
- ghnova/resource/resource.py +88 -0
- ghnova/user/__init__.py +8 -0
- ghnova/user/async_user.py +285 -0
- ghnova/user/base.py +214 -0
- ghnova/user/user.py +285 -0
- ghnova/utils/__init__.py +16 -0
- ghnova/utils/log.py +70 -0
- ghnova/utils/response.py +67 -0
- ghnova/version.py +11 -0
- ghnova-0.3.0.dist-info/METADATA +194 -0
- ghnova-0.3.0.dist-info/RECORD +60 -0
- ghnova-0.3.0.dist-info/WHEEL +4 -0
- ghnova-0.3.0.dist-info/entry_points.txt +2 -0
- ghnova-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Update command for user CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def update_command( # noqa: PLR0913
|
|
11
|
+
ctx: typer.Context,
|
|
12
|
+
account_name: Annotated[
|
|
13
|
+
str | None,
|
|
14
|
+
typer.Option(
|
|
15
|
+
"--account-name",
|
|
16
|
+
help="Name of the account to use for authentication.",
|
|
17
|
+
),
|
|
18
|
+
] = None,
|
|
19
|
+
token: Annotated[
|
|
20
|
+
str | None,
|
|
21
|
+
typer.Option(
|
|
22
|
+
"--token",
|
|
23
|
+
help="Token for authentication. If not provided, the token from the specified account will be used.",
|
|
24
|
+
),
|
|
25
|
+
] = None,
|
|
26
|
+
base_url: Annotated[
|
|
27
|
+
str | None,
|
|
28
|
+
typer.Option(
|
|
29
|
+
"--base-url",
|
|
30
|
+
help="Base URL of the GitHub platform. If not provided, the base URL from the specified account will be used.",
|
|
31
|
+
),
|
|
32
|
+
] = None,
|
|
33
|
+
name: Annotated[
|
|
34
|
+
str | None,
|
|
35
|
+
typer.Option(
|
|
36
|
+
"--name",
|
|
37
|
+
help="The name of the user.",
|
|
38
|
+
),
|
|
39
|
+
] = None,
|
|
40
|
+
email: Annotated[
|
|
41
|
+
str | None,
|
|
42
|
+
typer.Option(
|
|
43
|
+
"--email",
|
|
44
|
+
help="The email of the user.",
|
|
45
|
+
),
|
|
46
|
+
] = None,
|
|
47
|
+
blog: Annotated[
|
|
48
|
+
str | None,
|
|
49
|
+
typer.Option(
|
|
50
|
+
"--blog",
|
|
51
|
+
help="The blog URL of the user.",
|
|
52
|
+
),
|
|
53
|
+
] = None,
|
|
54
|
+
twitter_username: Annotated[
|
|
55
|
+
str | None,
|
|
56
|
+
typer.Option(
|
|
57
|
+
"--twitter-username",
|
|
58
|
+
help="The Twitter username of the user.",
|
|
59
|
+
),
|
|
60
|
+
] = None,
|
|
61
|
+
company: Annotated[
|
|
62
|
+
str | None,
|
|
63
|
+
typer.Option(
|
|
64
|
+
"--company",
|
|
65
|
+
help="The company of the user.",
|
|
66
|
+
),
|
|
67
|
+
] = None,
|
|
68
|
+
location: Annotated[
|
|
69
|
+
str | None,
|
|
70
|
+
typer.Option(
|
|
71
|
+
"--location",
|
|
72
|
+
help="The location of the user.",
|
|
73
|
+
),
|
|
74
|
+
] = None,
|
|
75
|
+
hireable: Annotated[
|
|
76
|
+
bool | None,
|
|
77
|
+
typer.Option(
|
|
78
|
+
"--hireable",
|
|
79
|
+
help="The hireable status of the user.",
|
|
80
|
+
),
|
|
81
|
+
] = None,
|
|
82
|
+
bio: Annotated[
|
|
83
|
+
str | None,
|
|
84
|
+
typer.Option(
|
|
85
|
+
"--bio",
|
|
86
|
+
help="The bio of the user.",
|
|
87
|
+
),
|
|
88
|
+
] = None,
|
|
89
|
+
etag: Annotated[
|
|
90
|
+
str | None,
|
|
91
|
+
typer.Option(
|
|
92
|
+
"--etag",
|
|
93
|
+
help="ETag from a previous request for caching purposes.",
|
|
94
|
+
),
|
|
95
|
+
] = None,
|
|
96
|
+
last_modified: Annotated[
|
|
97
|
+
str | None,
|
|
98
|
+
typer.Option(
|
|
99
|
+
"--last-modified",
|
|
100
|
+
help="Last-Modified header from a previous request for caching purposes.",
|
|
101
|
+
),
|
|
102
|
+
] = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Update the authenticated user's information on GitHub.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
ctx: Typer context.
|
|
108
|
+
account_name: Name of the account to use for authentication.
|
|
109
|
+
token: Token for authentication.
|
|
110
|
+
base_url: Base URL of the GitHub platform.
|
|
111
|
+
name: The name of the user.
|
|
112
|
+
email: The email of the user.
|
|
113
|
+
blog: The blog URL of the user.
|
|
114
|
+
twitter_username: The Twitter username of the user.
|
|
115
|
+
company: The company of the user.
|
|
116
|
+
location: The location of the user.
|
|
117
|
+
hireable: The hireable status of the user.
|
|
118
|
+
bio: The bio of the user.
|
|
119
|
+
etag: ETag from a previous request for caching purposes.
|
|
120
|
+
last_modified: Last-Modified header from a previous request for caching purposes.
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
import json # noqa: PLC0415
|
|
124
|
+
import logging # noqa: PLC0415
|
|
125
|
+
|
|
126
|
+
from ghnova.cli.utils.auth import get_auth_params # noqa: PLC0415
|
|
127
|
+
from ghnova.client.github import GitHub # noqa: PLC0415
|
|
128
|
+
|
|
129
|
+
logger = logging.getLogger("ghnova")
|
|
130
|
+
|
|
131
|
+
token, base_url = get_auth_params(
|
|
132
|
+
config_path=ctx.obj["config_path"],
|
|
133
|
+
account_name=account_name,
|
|
134
|
+
token=token,
|
|
135
|
+
base_url=base_url,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
with GitHub(token=token, base_url=base_url) as client:
|
|
140
|
+
user_client = client.user
|
|
141
|
+
data, status_code, etag_value, last_modified_value = user_client.update_user(
|
|
142
|
+
name=name,
|
|
143
|
+
email=email,
|
|
144
|
+
blog=blog,
|
|
145
|
+
twitter_username=twitter_username,
|
|
146
|
+
company=company,
|
|
147
|
+
location=location,
|
|
148
|
+
hireable=hireable,
|
|
149
|
+
bio=bio,
|
|
150
|
+
etag=etag,
|
|
151
|
+
last_modified=last_modified,
|
|
152
|
+
)
|
|
153
|
+
result = {
|
|
154
|
+
"data": data,
|
|
155
|
+
"metadata": {
|
|
156
|
+
"status_code": status_code,
|
|
157
|
+
"etag": etag_value,
|
|
158
|
+
"last_modified": last_modified_value,
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
print(json.dumps(result, indent=2, default=str))
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error("Error updating user information: %s", e)
|
|
164
|
+
raise typer.Exit(code=1) from e
|
ghnova/cli/utils/auth.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Utilities for authentication in the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ghnova.config.manager import ConfigManager
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("ghnova")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_auth_params(
|
|
14
|
+
config_path: Path | str,
|
|
15
|
+
account_name: str | None,
|
|
16
|
+
token: str | None,
|
|
17
|
+
base_url: str | None,
|
|
18
|
+
) -> tuple[str, str]:
|
|
19
|
+
"""Get authentication parameters from CLI context.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
config_path: Path to the configuration file.
|
|
23
|
+
account_name: Name of the account to use for authentication.
|
|
24
|
+
token: Token for authentication.
|
|
25
|
+
base_url: Base URL of the GitHub platform.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A tuple containing the token and base URL for authentication.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
if account_name is not None:
|
|
32
|
+
if token is not None or base_url is not None:
|
|
33
|
+
logger.warning(
|
|
34
|
+
"Both account name and token/base_url provided. The token and base_url from the account '%s' will be used.",
|
|
35
|
+
account_name,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
config_manager = ConfigManager(filename=config_path)
|
|
39
|
+
config_manager.load_config()
|
|
40
|
+
account_config = config_manager.get_config(name=account_name)
|
|
41
|
+
token = account_config.token
|
|
42
|
+
base_url = account_config.base_url
|
|
43
|
+
return token, base_url
|
|
44
|
+
if token is None and base_url is None:
|
|
45
|
+
config_manager = ConfigManager(filename=config_path)
|
|
46
|
+
config_manager.load_config()
|
|
47
|
+
|
|
48
|
+
if config_manager.has_default_account():
|
|
49
|
+
account_config = config_manager.get_config(name=None)
|
|
50
|
+
token = account_config.token
|
|
51
|
+
base_url = account_config.base_url
|
|
52
|
+
return token, base_url
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"No default account available for authentication. Please provide an account name or token/base_url."
|
|
56
|
+
)
|
|
57
|
+
if token is None or base_url is None:
|
|
58
|
+
missing_params = []
|
|
59
|
+
if token is None:
|
|
60
|
+
missing_params.append("token")
|
|
61
|
+
if base_url is None:
|
|
62
|
+
missing_params.append("base_url")
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Insufficient authentication parameters. Missing: {', '.join(missing_params)}. "
|
|
65
|
+
f"Please provide both token and base_url, or use an account name."
|
|
66
|
+
)
|
|
67
|
+
return token, base_url
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Asynchronous GitHub API client implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from aiohttp import ClientResponse, ClientSession, ClientTimeout
|
|
8
|
+
|
|
9
|
+
from ghnova.client.base import Client
|
|
10
|
+
from ghnova.issue.async_issue import AsyncIssue
|
|
11
|
+
from ghnova.user.async_user import AsyncUser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncGitHub(Client):
|
|
15
|
+
"""Asynchronous GitHub API client."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, token: str | None = None, base_url: str = "https://github.com") -> None:
|
|
18
|
+
"""Initialize the asynchronous GitHub client.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
token: The API token for authentication.
|
|
22
|
+
base_url: The base URL of the GitHub instance.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(token=token, base_url=base_url)
|
|
26
|
+
self.session: ClientSession | None = None
|
|
27
|
+
self.issue = AsyncIssue(client=self)
|
|
28
|
+
self.user = AsyncUser(client=self)
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
"""Return a string representation of the AsyncGitHub client.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
str: String representation.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
return f"<AsyncGitHub base_url={self.base_url}>"
|
|
38
|
+
|
|
39
|
+
async def __aenter__(self) -> AsyncGitHub:
|
|
40
|
+
"""Enter the asynchronous context manager.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The AsyncGitHub client instance.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
if self.session is not None and not self.session.closed:
|
|
47
|
+
raise RuntimeError("AsyncGitHub session already open; do not re-enter context manager.")
|
|
48
|
+
self.session = ClientSession(headers=self.headers)
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
52
|
+
"""Exit the asynchronous context manager.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
exc_type: The exception type.
|
|
56
|
+
exc_val: The exception value.
|
|
57
|
+
exc_tb: The traceback.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
if self.session:
|
|
61
|
+
await self.session.close()
|
|
62
|
+
self.session = None
|
|
63
|
+
|
|
64
|
+
def _get_session(self, headers: dict | None = None, **kwargs: Any) -> ClientSession:
|
|
65
|
+
"""Get or create the aiohttp ClientSession.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
headers: Optional headers to include in the session.
|
|
69
|
+
**kwargs: Additional arguments for ClientSession.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The aiohttp ClientSession instance.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
return ClientSession(headers=headers, **kwargs)
|
|
76
|
+
|
|
77
|
+
async def _request( # noqa: PLR0913
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
endpoint: str,
|
|
81
|
+
etag: str | None = None,
|
|
82
|
+
last_modified: str | None = None,
|
|
83
|
+
headers: dict | None = None,
|
|
84
|
+
timeout: int = 30,
|
|
85
|
+
**kwargs: Any,
|
|
86
|
+
) -> ClientResponse:
|
|
87
|
+
"""Make an asynchronous HTTP request to the GitHub API.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
method: The HTTP method (GET, POST, etc.).
|
|
91
|
+
endpoint: The API endpoint.
|
|
92
|
+
etag: str | None = None,
|
|
93
|
+
last_modified: str | None = None,
|
|
94
|
+
headers: Optional headers to include in the request.
|
|
95
|
+
timeout: Request timeout in seconds.
|
|
96
|
+
**kwargs: Additional arguments for the request.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
The HTTP response.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
if self.session is None:
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
"AsyncGitHub must be used as an async context manager. "
|
|
105
|
+
+ "Use 'async with AsyncGitHub(...) as client:' to ensure proper resource cleanup."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
url = self._build_url(endpoint=endpoint)
|
|
109
|
+
conditional_headers = self._get_conditional_request_headers(etag=etag, last_modified=last_modified)
|
|
110
|
+
request_headers = {**self.headers, **conditional_headers, **(headers or {})}
|
|
111
|
+
timeout_obj = ClientTimeout(total=timeout)
|
|
112
|
+
response = await self.session.request(
|
|
113
|
+
method=method, url=url, headers=request_headers, timeout=timeout_obj, **kwargs
|
|
114
|
+
)
|
|
115
|
+
try:
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
except Exception:
|
|
118
|
+
response.release()
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
return response
|
ghnova/client/base.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Base client class for GitHub API interactions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import urllib.parse
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Client:
|
|
10
|
+
"""Abstract base class for GitHub clients."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, token: str | None, base_url: str) -> None:
|
|
13
|
+
"""Construct the base client.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
token: The API token for authentication.
|
|
17
|
+
base_url: The base URL of the GitHub instance.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
self.token = token
|
|
21
|
+
self.base_url = base_url.rstrip("/")
|
|
22
|
+
self.headers: dict[str, Any] = {}
|
|
23
|
+
if self.token:
|
|
24
|
+
self.headers["Authorization"] = f"Bearer {self.token}"
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
"""Return a string representation of the client.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
str: String representation.
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
return f"<Client base_url={self.base_url}>"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def api_url(self) -> str:
|
|
37
|
+
"""Return the base API URL.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str: The base API URL.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
if urllib.parse.urlparse(self.base_url).netloc == "github.com":
|
|
44
|
+
return "https://api.github.com"
|
|
45
|
+
else:
|
|
46
|
+
return f"{self.base_url}/api/v3"
|
|
47
|
+
|
|
48
|
+
def _build_url(self, endpoint: str) -> str:
|
|
49
|
+
"""Construct the full URL for a given endpoint.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
endpoint (str): The API endpoint.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
str: The full URL.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
return f"{self.api_url}/{endpoint.lstrip('/')}"
|
|
59
|
+
|
|
60
|
+
def _get_conditional_request_headers(
|
|
61
|
+
self, etag: str | None = None, last_modified: str | None = None
|
|
62
|
+
) -> dict[str, str]:
|
|
63
|
+
"""Get headers for conditional requests.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
etag: The ETag value for the resource.
|
|
67
|
+
last_modified: The Last-Modified timestamp for the resource.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A dictionary of headers for the conditional request.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
headers: dict[str, str] = {}
|
|
74
|
+
if etag:
|
|
75
|
+
headers["If-None-Match"] = etag
|
|
76
|
+
if last_modified:
|
|
77
|
+
headers["If-Modified-Since"] = last_modified
|
|
78
|
+
return headers
|
ghnova/client/github.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""GitHub API client implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from requests import Response
|
|
9
|
+
|
|
10
|
+
from ghnova.client.base import Client
|
|
11
|
+
from ghnova.issue.issue import Issue
|
|
12
|
+
from ghnova.repository.repository import Repository
|
|
13
|
+
from ghnova.user.user import User
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GitHub(Client):
|
|
17
|
+
"""Synchronous GitHub API client."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, token: str | None = None, base_url: str = "https://github.com") -> None:
|
|
20
|
+
"""Initialize the GitHub client.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
token: The API token for authentication.
|
|
24
|
+
base_url: The base URL of the GitHub instance.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(token=token, base_url=base_url)
|
|
28
|
+
self.session: requests.Session | None = None
|
|
29
|
+
self.issue = Issue(client=self)
|
|
30
|
+
self.repository = Repository(client=self)
|
|
31
|
+
self.user = User(client=self)
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
"""Return a string representation of the GitHub client.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: String representation.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
return f"<GitHub base_url={self.base_url}>"
|
|
41
|
+
|
|
42
|
+
def __enter__(self) -> GitHub:
|
|
43
|
+
"""Enter the context manager.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The GitHub client instance.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
if self.session is not None:
|
|
50
|
+
raise RuntimeError("GitHub session already open; do not re-enter context manager.")
|
|
51
|
+
self.session = requests.Session()
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
55
|
+
"""Exit the context manager.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
exc_type: The exception type.
|
|
59
|
+
exc_val: The exception value.
|
|
60
|
+
exc_tb: The traceback.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
if self.session:
|
|
64
|
+
self.session.close()
|
|
65
|
+
self.session = None
|
|
66
|
+
|
|
67
|
+
def _request( # noqa: PLR0913
|
|
68
|
+
self,
|
|
69
|
+
method: str,
|
|
70
|
+
endpoint: str,
|
|
71
|
+
etag: str | None = None,
|
|
72
|
+
last_modified: str | None = None,
|
|
73
|
+
headers: dict | None = None,
|
|
74
|
+
timeout: int = 30,
|
|
75
|
+
**kwargs: Any,
|
|
76
|
+
) -> Response:
|
|
77
|
+
"""Make an HTTP request to the GitHub API.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
method: The HTTP method (GET, POST, etc.).
|
|
81
|
+
endpoint: The API endpoint.
|
|
82
|
+
etag: The ETag value for conditional requests.
|
|
83
|
+
last_modified: The Last-Modified timestamp for conditional requests.
|
|
84
|
+
headers: Additional headers for the request.
|
|
85
|
+
timeout: Timeout for the request in seconds.
|
|
86
|
+
**kwargs: Additional arguments for the request.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The HTTP response.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
if self.session is None:
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
"GitHub must be used as a context manager. "
|
|
95
|
+
+ "Use 'with GitHub(...) as client:' to ensure proper resource cleanup."
|
|
96
|
+
)
|
|
97
|
+
url = self._build_url(endpoint=endpoint)
|
|
98
|
+
conditional_headers = self._get_conditional_request_headers(etag=etag, last_modified=last_modified)
|
|
99
|
+
request_headers = {**self.headers, **conditional_headers, **(headers or {})}
|
|
100
|
+
response = self.session.request(method, url, headers=request_headers, timeout=timeout, **kwargs)
|
|
101
|
+
try:
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
except Exception:
|
|
104
|
+
response.close()
|
|
105
|
+
raise
|
|
106
|
+
|
|
107
|
+
return response
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Initialization of the configuration module for ghnova."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ghnova.config.manager import ConfigManager
|
|
6
|
+
from ghnova.config.model import AccountConfig, Config
|
|
7
|
+
|
|
8
|
+
__all__ = ["AccountConfig", "Config", "ConfigManager"]
|