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.
Files changed (45) hide show
  1. devrev/__init__.py +47 -0
  2. devrev/client.py +343 -0
  3. devrev/config.py +180 -0
  4. devrev/exceptions.py +205 -0
  5. devrev/models/__init__.py +499 -0
  6. devrev/models/accounts.py +187 -0
  7. devrev/models/articles.py +109 -0
  8. devrev/models/base.py +147 -0
  9. devrev/models/code_changes.py +103 -0
  10. devrev/models/conversations.py +115 -0
  11. devrev/models/dev_users.py +258 -0
  12. devrev/models/groups.py +140 -0
  13. devrev/models/links.py +107 -0
  14. devrev/models/parts.py +110 -0
  15. devrev/models/rev_users.py +177 -0
  16. devrev/models/slas.py +112 -0
  17. devrev/models/tags.py +90 -0
  18. devrev/models/timeline_entries.py +100 -0
  19. devrev/models/webhooks.py +109 -0
  20. devrev/models/works.py +280 -0
  21. devrev/py.typed +1 -0
  22. devrev/services/__init__.py +74 -0
  23. devrev/services/accounts.py +325 -0
  24. devrev/services/articles.py +80 -0
  25. devrev/services/base.py +234 -0
  26. devrev/services/code_changes.py +80 -0
  27. devrev/services/conversations.py +98 -0
  28. devrev/services/dev_users.py +401 -0
  29. devrev/services/groups.py +103 -0
  30. devrev/services/links.py +68 -0
  31. devrev/services/parts.py +100 -0
  32. devrev/services/rev_users.py +235 -0
  33. devrev/services/slas.py +82 -0
  34. devrev/services/tags.py +80 -0
  35. devrev/services/timeline_entries.py +80 -0
  36. devrev/services/webhooks.py +80 -0
  37. devrev/services/works.py +363 -0
  38. devrev/utils/__init__.py +14 -0
  39. devrev/utils/deprecation.py +49 -0
  40. devrev/utils/http.py +521 -0
  41. devrev/utils/logging.py +139 -0
  42. devrev/utils/pagination.py +155 -0
  43. devrev_python_sdk-1.0.0.dist-info/METADATA +774 -0
  44. devrev_python_sdk-1.0.0.dist-info/RECORD +45 -0
  45. 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