devrev-Python-SDK 1.0.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.
- devrev/__init__.py +47 -0
- devrev/client.py +343 -0
- devrev/config.py +180 -0
- devrev/exceptions.py +205 -0
- devrev/models/__init__.py +499 -0
- devrev/models/accounts.py +187 -0
- devrev/models/articles.py +109 -0
- devrev/models/base.py +147 -0
- devrev/models/code_changes.py +103 -0
- devrev/models/conversations.py +115 -0
- devrev/models/dev_users.py +258 -0
- devrev/models/groups.py +140 -0
- devrev/models/links.py +107 -0
- devrev/models/parts.py +110 -0
- devrev/models/rev_users.py +177 -0
- devrev/models/slas.py +112 -0
- devrev/models/tags.py +90 -0
- devrev/models/timeline_entries.py +100 -0
- devrev/models/webhooks.py +109 -0
- devrev/models/works.py +280 -0
- devrev/py.typed +1 -0
- devrev/services/__init__.py +74 -0
- devrev/services/accounts.py +325 -0
- devrev/services/articles.py +80 -0
- devrev/services/base.py +234 -0
- devrev/services/code_changes.py +80 -0
- devrev/services/conversations.py +98 -0
- devrev/services/dev_users.py +401 -0
- devrev/services/groups.py +103 -0
- devrev/services/links.py +68 -0
- devrev/services/parts.py +100 -0
- devrev/services/rev_users.py +235 -0
- devrev/services/slas.py +82 -0
- devrev/services/tags.py +80 -0
- devrev/services/timeline_entries.py +80 -0
- devrev/services/webhooks.py +80 -0
- devrev/services/works.py +363 -0
- devrev/utils/__init__.py +14 -0
- devrev/utils/deprecation.py +49 -0
- devrev/utils/http.py +521 -0
- devrev/utils/logging.py +139 -0
- devrev/utils/pagination.py +155 -0
- devrev_python_sdk-1.0.0.dist-info/METADATA +774 -0
- devrev_python_sdk-1.0.0.dist-info/RECORD +45 -0
- devrev_python_sdk-1.0.0.dist-info/WHEEL +4 -0
devrev/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""DevRev Python SDK.
|
|
2
|
+
|
|
3
|
+
A modern, type-safe Python SDK for the DevRev API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from devrev.client import AsyncDevRevClient, DevRevClient
|
|
7
|
+
from devrev.config import DevRevConfig, configure, get_config
|
|
8
|
+
from devrev.exceptions import (
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
ConfigurationError,
|
|
11
|
+
ConflictError,
|
|
12
|
+
DevRevError,
|
|
13
|
+
ForbiddenError,
|
|
14
|
+
NetworkError,
|
|
15
|
+
NotFoundError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
ServerError,
|
|
18
|
+
ServiceUnavailableError,
|
|
19
|
+
TimeoutError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Version
|
|
26
|
+
"__version__",
|
|
27
|
+
# Clients
|
|
28
|
+
"DevRevClient",
|
|
29
|
+
"AsyncDevRevClient",
|
|
30
|
+
# Configuration
|
|
31
|
+
"DevRevConfig",
|
|
32
|
+
"get_config",
|
|
33
|
+
"configure",
|
|
34
|
+
# Exceptions
|
|
35
|
+
"DevRevError",
|
|
36
|
+
"AuthenticationError",
|
|
37
|
+
"ForbiddenError",
|
|
38
|
+
"NotFoundError",
|
|
39
|
+
"ValidationError",
|
|
40
|
+
"ConflictError",
|
|
41
|
+
"RateLimitError",
|
|
42
|
+
"ServerError",
|
|
43
|
+
"ServiceUnavailableError",
|
|
44
|
+
"ConfigurationError",
|
|
45
|
+
"TimeoutError",
|
|
46
|
+
"NetworkError",
|
|
47
|
+
]
|
devrev/client.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""DevRev SDK Client.
|
|
2
|
+
|
|
3
|
+
Main client classes for interacting with the DevRev API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Self
|
|
7
|
+
|
|
8
|
+
from devrev.config import DevRevConfig, get_config
|
|
9
|
+
from devrev.services.accounts import AccountsService, AsyncAccountsService
|
|
10
|
+
from devrev.services.articles import ArticlesService, AsyncArticlesService
|
|
11
|
+
from devrev.services.code_changes import AsyncCodeChangesService, CodeChangesService
|
|
12
|
+
from devrev.services.conversations import (
|
|
13
|
+
AsyncConversationsService,
|
|
14
|
+
ConversationsService,
|
|
15
|
+
)
|
|
16
|
+
from devrev.services.dev_users import AsyncDevUsersService, DevUsersService
|
|
17
|
+
from devrev.services.groups import AsyncGroupsService, GroupsService
|
|
18
|
+
from devrev.services.links import AsyncLinksService, LinksService
|
|
19
|
+
from devrev.services.parts import AsyncPartsService, PartsService
|
|
20
|
+
from devrev.services.rev_users import AsyncRevUsersService, RevUsersService
|
|
21
|
+
from devrev.services.slas import AsyncSlasService, SlasService
|
|
22
|
+
from devrev.services.tags import AsyncTagsService, TagsService
|
|
23
|
+
from devrev.services.timeline_entries import (
|
|
24
|
+
AsyncTimelineEntriesService,
|
|
25
|
+
TimelineEntriesService,
|
|
26
|
+
)
|
|
27
|
+
from devrev.services.webhooks import AsyncWebhooksService, WebhooksService
|
|
28
|
+
from devrev.services.works import AsyncWorksService, WorksService
|
|
29
|
+
from devrev.utils.http import AsyncHTTPClient, HTTPClient
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DevRevClient:
|
|
33
|
+
"""Synchronous DevRev API client.
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
```python
|
|
37
|
+
from devrev import DevRevClient
|
|
38
|
+
|
|
39
|
+
# Initialize with environment variables
|
|
40
|
+
client = DevRevClient()
|
|
41
|
+
|
|
42
|
+
# Or with explicit configuration
|
|
43
|
+
client = DevRevClient(api_token="your-token")
|
|
44
|
+
|
|
45
|
+
# Access services
|
|
46
|
+
accounts = client.accounts.list()
|
|
47
|
+
works = client.works.get("work:123")
|
|
48
|
+
```
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
api_token: str | None = None,
|
|
55
|
+
base_url: str | None = None,
|
|
56
|
+
timeout: int | None = None,
|
|
57
|
+
config: DevRevConfig | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize the DevRev client.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
api_token: DevRev API token (or set DEVREV_API_TOKEN env var)
|
|
63
|
+
base_url: API base URL (default: https://api.devrev.ai)
|
|
64
|
+
timeout: Request timeout in seconds (default: 30)
|
|
65
|
+
config: Full configuration object (overrides other params)
|
|
66
|
+
"""
|
|
67
|
+
if config:
|
|
68
|
+
self._config = config
|
|
69
|
+
else:
|
|
70
|
+
config_kwargs: dict[str, str | int] = {}
|
|
71
|
+
if api_token:
|
|
72
|
+
config_kwargs["api_token"] = api_token
|
|
73
|
+
if base_url:
|
|
74
|
+
config_kwargs["base_url"] = base_url
|
|
75
|
+
if timeout:
|
|
76
|
+
config_kwargs["timeout"] = timeout
|
|
77
|
+
|
|
78
|
+
if config_kwargs:
|
|
79
|
+
self._config = DevRevConfig(**config_kwargs) # type: ignore[arg-type]
|
|
80
|
+
else:
|
|
81
|
+
self._config = get_config()
|
|
82
|
+
|
|
83
|
+
# Initialize HTTP client
|
|
84
|
+
self._http = HTTPClient(
|
|
85
|
+
base_url=self._config.base_url,
|
|
86
|
+
api_token=self._config.api_token,
|
|
87
|
+
timeout=self._config.timeout,
|
|
88
|
+
max_retries=self._config.max_retries,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Initialize services
|
|
92
|
+
self._accounts = AccountsService(self._http)
|
|
93
|
+
self._articles = ArticlesService(self._http)
|
|
94
|
+
self._code_changes = CodeChangesService(self._http)
|
|
95
|
+
self._conversations = ConversationsService(self._http)
|
|
96
|
+
self._dev_users = DevUsersService(self._http)
|
|
97
|
+
self._groups = GroupsService(self._http)
|
|
98
|
+
self._links = LinksService(self._http)
|
|
99
|
+
self._parts = PartsService(self._http)
|
|
100
|
+
self._rev_users = RevUsersService(self._http)
|
|
101
|
+
self._slas = SlasService(self._http)
|
|
102
|
+
self._tags = TagsService(self._http)
|
|
103
|
+
self._timeline_entries = TimelineEntriesService(self._http)
|
|
104
|
+
self._webhooks = WebhooksService(self._http)
|
|
105
|
+
self._works = WorksService(self._http)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def accounts(self) -> AccountsService:
|
|
109
|
+
"""Access the Accounts service."""
|
|
110
|
+
return self._accounts
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def articles(self) -> ArticlesService:
|
|
114
|
+
"""Access the Articles service."""
|
|
115
|
+
return self._articles
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def code_changes(self) -> CodeChangesService:
|
|
119
|
+
"""Access the Code Changes service."""
|
|
120
|
+
return self._code_changes
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def conversations(self) -> ConversationsService:
|
|
124
|
+
"""Access the Conversations service."""
|
|
125
|
+
return self._conversations
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def dev_users(self) -> DevUsersService:
|
|
129
|
+
"""Access the Dev Users service."""
|
|
130
|
+
return self._dev_users
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def groups(self) -> GroupsService:
|
|
134
|
+
"""Access the Groups service."""
|
|
135
|
+
return self._groups
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def links(self) -> LinksService:
|
|
139
|
+
"""Access the Links service."""
|
|
140
|
+
return self._links
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def parts(self) -> PartsService:
|
|
144
|
+
"""Access the Parts service."""
|
|
145
|
+
return self._parts
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def rev_users(self) -> RevUsersService:
|
|
149
|
+
"""Access the Rev Users service."""
|
|
150
|
+
return self._rev_users
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def slas(self) -> SlasService:
|
|
154
|
+
"""Access the SLAs service."""
|
|
155
|
+
return self._slas
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def tags(self) -> TagsService:
|
|
159
|
+
"""Access the Tags service."""
|
|
160
|
+
return self._tags
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def timeline_entries(self) -> TimelineEntriesService:
|
|
164
|
+
"""Access the Timeline Entries service."""
|
|
165
|
+
return self._timeline_entries
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def webhooks(self) -> WebhooksService:
|
|
169
|
+
"""Access the Webhooks service."""
|
|
170
|
+
return self._webhooks
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def works(self) -> WorksService:
|
|
174
|
+
"""Access the Works service."""
|
|
175
|
+
return self._works
|
|
176
|
+
|
|
177
|
+
def close(self) -> None:
|
|
178
|
+
"""Close the client and release resources."""
|
|
179
|
+
self._http.close()
|
|
180
|
+
|
|
181
|
+
def __enter__(self) -> Self:
|
|
182
|
+
"""Enter context manager."""
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
def __exit__(self, *args: object) -> None:
|
|
186
|
+
"""Exit context manager."""
|
|
187
|
+
self.close()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class AsyncDevRevClient:
|
|
191
|
+
"""Asynchronous DevRev API client.
|
|
192
|
+
|
|
193
|
+
Usage:
|
|
194
|
+
```python
|
|
195
|
+
import asyncio
|
|
196
|
+
from devrev import AsyncDevRevClient
|
|
197
|
+
|
|
198
|
+
async def main():
|
|
199
|
+
async with AsyncDevRevClient() as client:
|
|
200
|
+
accounts = await client.accounts.list()
|
|
201
|
+
work = await client.works.get("work:123")
|
|
202
|
+
|
|
203
|
+
asyncio.run(main())
|
|
204
|
+
```
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
api_token: str | None = None,
|
|
211
|
+
base_url: str | None = None,
|
|
212
|
+
timeout: int | None = None,
|
|
213
|
+
config: DevRevConfig | None = None,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Initialize the async DevRev client.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
api_token: DevRev API token (or set DEVREV_API_TOKEN env var)
|
|
219
|
+
base_url: API base URL (default: https://api.devrev.ai)
|
|
220
|
+
timeout: Request timeout in seconds (default: 30)
|
|
221
|
+
config: Full configuration object (overrides other params)
|
|
222
|
+
"""
|
|
223
|
+
if config:
|
|
224
|
+
self._config = config
|
|
225
|
+
else:
|
|
226
|
+
config_kwargs: dict[str, str | int] = {}
|
|
227
|
+
if api_token:
|
|
228
|
+
config_kwargs["api_token"] = api_token
|
|
229
|
+
if base_url:
|
|
230
|
+
config_kwargs["base_url"] = base_url
|
|
231
|
+
if timeout:
|
|
232
|
+
config_kwargs["timeout"] = timeout
|
|
233
|
+
|
|
234
|
+
if config_kwargs:
|
|
235
|
+
self._config = DevRevConfig(**config_kwargs) # type: ignore[arg-type]
|
|
236
|
+
else:
|
|
237
|
+
self._config = get_config()
|
|
238
|
+
|
|
239
|
+
# Initialize async HTTP client
|
|
240
|
+
self._http = AsyncHTTPClient(
|
|
241
|
+
base_url=self._config.base_url,
|
|
242
|
+
api_token=self._config.api_token,
|
|
243
|
+
timeout=self._config.timeout,
|
|
244
|
+
max_retries=self._config.max_retries,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Initialize async services
|
|
248
|
+
self._accounts = AsyncAccountsService(self._http)
|
|
249
|
+
self._articles = AsyncArticlesService(self._http)
|
|
250
|
+
self._code_changes = AsyncCodeChangesService(self._http)
|
|
251
|
+
self._conversations = AsyncConversationsService(self._http)
|
|
252
|
+
self._dev_users = AsyncDevUsersService(self._http)
|
|
253
|
+
self._groups = AsyncGroupsService(self._http)
|
|
254
|
+
self._links = AsyncLinksService(self._http)
|
|
255
|
+
self._parts = AsyncPartsService(self._http)
|
|
256
|
+
self._rev_users = AsyncRevUsersService(self._http)
|
|
257
|
+
self._slas = AsyncSlasService(self._http)
|
|
258
|
+
self._tags = AsyncTagsService(self._http)
|
|
259
|
+
self._timeline_entries = AsyncTimelineEntriesService(self._http)
|
|
260
|
+
self._webhooks = AsyncWebhooksService(self._http)
|
|
261
|
+
self._works = AsyncWorksService(self._http)
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def accounts(self) -> AsyncAccountsService:
|
|
265
|
+
"""Access the Accounts service."""
|
|
266
|
+
return self._accounts
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def articles(self) -> AsyncArticlesService:
|
|
270
|
+
"""Access the Articles service."""
|
|
271
|
+
return self._articles
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def code_changes(self) -> AsyncCodeChangesService:
|
|
275
|
+
"""Access the Code Changes service."""
|
|
276
|
+
return self._code_changes
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def conversations(self) -> AsyncConversationsService:
|
|
280
|
+
"""Access the Conversations service."""
|
|
281
|
+
return self._conversations
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def dev_users(self) -> AsyncDevUsersService:
|
|
285
|
+
"""Access the Dev Users service."""
|
|
286
|
+
return self._dev_users
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def groups(self) -> AsyncGroupsService:
|
|
290
|
+
"""Access the Groups service."""
|
|
291
|
+
return self._groups
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def links(self) -> AsyncLinksService:
|
|
295
|
+
"""Access the Links service."""
|
|
296
|
+
return self._links
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def parts(self) -> AsyncPartsService:
|
|
300
|
+
"""Access the Parts service."""
|
|
301
|
+
return self._parts
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def rev_users(self) -> AsyncRevUsersService:
|
|
305
|
+
"""Access the Rev Users service."""
|
|
306
|
+
return self._rev_users
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def slas(self) -> AsyncSlasService:
|
|
310
|
+
"""Access the SLAs service."""
|
|
311
|
+
return self._slas
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def tags(self) -> AsyncTagsService:
|
|
315
|
+
"""Access the Tags service."""
|
|
316
|
+
return self._tags
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def timeline_entries(self) -> AsyncTimelineEntriesService:
|
|
320
|
+
"""Access the Timeline Entries service."""
|
|
321
|
+
return self._timeline_entries
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def webhooks(self) -> AsyncWebhooksService:
|
|
325
|
+
"""Access the Webhooks service."""
|
|
326
|
+
return self._webhooks
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def works(self) -> AsyncWorksService:
|
|
330
|
+
"""Access the Works service."""
|
|
331
|
+
return self._works
|
|
332
|
+
|
|
333
|
+
async def close(self) -> None:
|
|
334
|
+
"""Close the client and release resources."""
|
|
335
|
+
await self._http.close()
|
|
336
|
+
|
|
337
|
+
async def __aenter__(self) -> Self:
|
|
338
|
+
"""Enter async context manager."""
|
|
339
|
+
return self
|
|
340
|
+
|
|
341
|
+
async def __aexit__(self, *args: object) -> None:
|
|
342
|
+
"""Exit async context manager."""
|
|
343
|
+
await self.close()
|
devrev/config.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Configuration management for DevRev SDK.
|
|
2
|
+
|
|
3
|
+
This module provides configuration loading from environment variables
|
|
4
|
+
with optional .env file support for local development.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import Field, SecretStr, field_validator
|
|
10
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DevRevConfig(BaseSettings):
|
|
14
|
+
"""DevRev SDK Configuration.
|
|
15
|
+
|
|
16
|
+
Configuration is loaded from environment variables. For local development,
|
|
17
|
+
use a .env file (never commit this file!).
|
|
18
|
+
|
|
19
|
+
Environment Variables:
|
|
20
|
+
DEVREV_API_TOKEN: API authentication token (required)
|
|
21
|
+
DEVREV_BASE_URL: API base URL (default: https://api.devrev.ai)
|
|
22
|
+
DEVREV_TIMEOUT: Request timeout in seconds (default: 30)
|
|
23
|
+
DEVREV_MAX_RETRIES: Maximum retry attempts (default: 3)
|
|
24
|
+
DEVREV_LOG_LEVEL: Logging level (default: WARN)
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
```python
|
|
28
|
+
from devrev import DevRevConfig
|
|
29
|
+
|
|
30
|
+
# Load from environment
|
|
31
|
+
config = DevRevConfig()
|
|
32
|
+
|
|
33
|
+
# Or with explicit values
|
|
34
|
+
config = DevRevConfig(
|
|
35
|
+
api_token="your-token",
|
|
36
|
+
log_level="DEBUG",
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
model_config = SettingsConfigDict(
|
|
42
|
+
env_prefix="DEVREV_",
|
|
43
|
+
env_file=".env",
|
|
44
|
+
env_file_encoding="utf-8",
|
|
45
|
+
extra="ignore",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Authentication
|
|
49
|
+
api_token: SecretStr = Field(
|
|
50
|
+
...,
|
|
51
|
+
description="DevRev API authentication token",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# API Settings
|
|
55
|
+
base_url: str = Field(
|
|
56
|
+
default="https://api.devrev.ai",
|
|
57
|
+
description="DevRev API base URL",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# HTTP Settings
|
|
61
|
+
timeout: int = Field(
|
|
62
|
+
default=30,
|
|
63
|
+
ge=1,
|
|
64
|
+
le=300,
|
|
65
|
+
description="Request timeout in seconds",
|
|
66
|
+
)
|
|
67
|
+
max_retries: int = Field(
|
|
68
|
+
default=3,
|
|
69
|
+
ge=0,
|
|
70
|
+
le=10,
|
|
71
|
+
description="Maximum number of retry attempts",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Logging
|
|
75
|
+
log_level: Literal["DEBUG", "INFO", "WARN", "WARNING", "ERROR"] = Field(
|
|
76
|
+
default="WARN",
|
|
77
|
+
description="Logging level",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@field_validator("base_url")
|
|
81
|
+
@classmethod
|
|
82
|
+
def validate_base_url(cls, v: str) -> str:
|
|
83
|
+
"""Validate and normalize base URL.
|
|
84
|
+
|
|
85
|
+
Security: Enforces HTTPS-only connections to prevent credential leakage.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
v: The base URL value
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Normalized URL without trailing slash
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If URL uses insecure HTTP protocol
|
|
95
|
+
"""
|
|
96
|
+
url = v.rstrip("/")
|
|
97
|
+
# Security: Enforce HTTPS to prevent credential exposure
|
|
98
|
+
if url.startswith("http://"):
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"Insecure HTTP URLs are not allowed. Use HTTPS to protect your API credentials."
|
|
101
|
+
)
|
|
102
|
+
if not url.startswith("https://"):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"Base URL must start with 'https://'. "
|
|
105
|
+
f"Got: {url[:50]}..." # Truncate to avoid leaking full URL in errors
|
|
106
|
+
)
|
|
107
|
+
return url
|
|
108
|
+
|
|
109
|
+
@field_validator("log_level")
|
|
110
|
+
@classmethod
|
|
111
|
+
def normalize_log_level(
|
|
112
|
+
cls, v: Literal["DEBUG", "INFO", "WARN", "WARNING", "ERROR"]
|
|
113
|
+
) -> Literal["DEBUG", "INFO", "WARN", "WARNING", "ERROR"]:
|
|
114
|
+
"""Normalize WARNING to WARN for consistency."""
|
|
115
|
+
if v == "WARNING":
|
|
116
|
+
return "WARN"
|
|
117
|
+
return v
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Global configuration instance
|
|
121
|
+
_config: DevRevConfig | None = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_config() -> DevRevConfig:
|
|
125
|
+
"""Get or create the global configuration instance.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The global DevRevConfig instance
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
```python
|
|
132
|
+
from devrev import get_config
|
|
133
|
+
|
|
134
|
+
config = get_config()
|
|
135
|
+
print(f"Base URL: {config.base_url}")
|
|
136
|
+
```
|
|
137
|
+
"""
|
|
138
|
+
global _config
|
|
139
|
+
if _config is None:
|
|
140
|
+
_config = DevRevConfig()
|
|
141
|
+
# Auto-configure logging based on config
|
|
142
|
+
from devrev.utils.logging import configure_logging
|
|
143
|
+
|
|
144
|
+
configure_logging(level=_config.log_level)
|
|
145
|
+
return _config
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def configure(**kwargs: Any) -> DevRevConfig:
|
|
149
|
+
"""Configure the SDK with custom settings.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
**kwargs: Configuration options to override
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The new DevRevConfig instance
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
```python
|
|
159
|
+
from devrev import configure
|
|
160
|
+
|
|
161
|
+
config = configure(
|
|
162
|
+
api_token="your-token",
|
|
163
|
+
log_level="DEBUG",
|
|
164
|
+
timeout=60,
|
|
165
|
+
)
|
|
166
|
+
```
|
|
167
|
+
"""
|
|
168
|
+
global _config
|
|
169
|
+
_config = DevRevConfig(**kwargs)
|
|
170
|
+
# Auto-configure logging based on config
|
|
171
|
+
from devrev.utils.logging import configure_logging
|
|
172
|
+
|
|
173
|
+
configure_logging(level=_config.log_level)
|
|
174
|
+
return _config
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def reset_config() -> None:
|
|
178
|
+
"""Reset the global configuration (primarily for testing)."""
|
|
179
|
+
global _config
|
|
180
|
+
_config = None
|