tracktolib 0.68.0__tar.gz → 0.69.0__tar.gz

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 (31) hide show
  1. {tracktolib-0.68.0 → tracktolib-0.69.0}/PKG-INFO +95 -2
  2. {tracktolib-0.68.0 → tracktolib-0.69.0}/README.md +90 -1
  3. {tracktolib-0.68.0 → tracktolib-0.69.0}/pyproject.toml +15 -6
  4. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/api.py +12 -13
  5. tracktolib-0.69.0/tracktolib/cf/__init__.py +8 -0
  6. tracktolib-0.69.0/tracktolib/cf/client.py +149 -0
  7. tracktolib-0.69.0/tracktolib/cf/types.py +17 -0
  8. tracktolib-0.69.0/tracktolib/gh/__init__.py +11 -0
  9. tracktolib-0.69.0/tracktolib/gh/client.py +206 -0
  10. tracktolib-0.69.0/tracktolib/gh/types.py +203 -0
  11. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/http_utils.py +1 -1
  12. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/logs.py +1 -1
  13. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/models.py +0 -1
  14. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg/__init__.py +10 -10
  15. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg/query.py +1 -1
  16. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg/utils.py +5 -5
  17. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg_sync.py +3 -5
  18. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg_utils.py +1 -4
  19. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/s3/niquests.py +235 -32
  20. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/s3/s3.py +1 -1
  21. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/utils.py +12 -3
  22. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/__init__.py +0 -0
  23. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/__init__.py +0 -0
  24. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/blocks.py +0 -0
  25. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/cache.py +0 -0
  26. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/fetch.py +0 -0
  27. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/markdown.py +3 -3
  28. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/utils.py +5 -5
  29. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/s3/__init__.py +0 -0
  30. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/s3/minio.py +1 -1
  31. {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/tests.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tracktolib
3
- Version: 0.68.0
3
+ Version: 0.69.0
4
4
  Summary: Utility library for python
5
5
  Keywords: utility
6
6
  Author-email: julien.brayere@tracktor.fr
@@ -12,6 +12,8 @@ Classifier: Programming Language :: Python :: 3.13
12
12
  Classifier: Programming Language :: Python :: 3.14
13
13
  Requires-Dist: fastapi>=0.103.2 ; extra == 'api'
14
14
  Requires-Dist: pydantic>=2 ; extra == 'api'
15
+ Requires-Dist: niquests>=3.17.0 ; extra == 'cf'
16
+ Requires-Dist: niquests>=3.17.0 ; extra == 'gh'
15
17
  Requires-Dist: httpx>=0.25.0 ; extra == 'http'
16
18
  Requires-Dist: python-json-logger>=3.2.1 ; extra == 'logs'
17
19
  Requires-Dist: niquests>=3.17.0 ; extra == 'notion'
@@ -26,6 +28,8 @@ Requires-Dist: niquests>=3.17.0 ; extra == 's3-niquests'
26
28
  Requires-Dist: deepdiff>=8.1.0 ; extra == 'tests'
27
29
  Requires-Python: >=3.12, <4.0
28
30
  Provides-Extra: api
31
+ Provides-Extra: cf
32
+ Provides-Extra: gh
29
33
  Provides-Extra: http
30
34
  Provides-Extra: logs
31
35
  Provides-Extra: notion
@@ -126,12 +130,42 @@ uv add tracktolib[s3-minio]
126
130
 
127
131
  ### s3-niquests
128
132
 
129
- Async S3 helpers using [niquests](https://github.com/jawah/niquests) and [botocore](https://github.com/boto/botocore).
133
+ Async S3 helpers using [niquests](https://github.com/jawah/niquests) and [botocore](https://github.com/boto/botocore) presigned URLs.
130
134
 
131
135
  ```bash
132
136
  uv add tracktolib[s3-niquests]
133
137
  ```
134
138
 
139
+ ```python
140
+ from tracktolib.s3.niquests import S3Session
141
+
142
+ async with S3Session(
143
+ endpoint_url='http://localhost:9000',
144
+ access_key='...',
145
+ secret_key='...',
146
+ region='us-east-1',
147
+ ) as s3:
148
+ # Object operations
149
+ await s3.put_object('bucket', 'path/file.txt', b'content')
150
+ content = await s3.get_object('bucket', 'path/file.txt')
151
+ await s3.delete_object('bucket', 'path/file.txt')
152
+
153
+ # Streaming upload (multipart for large files)
154
+ async def data_stream():
155
+ yield b'chunk1'
156
+ yield b'chunk2'
157
+ await s3.file_upload('bucket', 'large-file.bin', data_stream())
158
+
159
+ # Bucket policy management
160
+ policy = {'Version': '2012-10-17', 'Statement': [...]}
161
+ await s3.put_bucket_policy('bucket', policy)
162
+ await s3.get_bucket_policy('bucket')
163
+ await s3.delete_bucket_policy('bucket')
164
+
165
+ # Empty a bucket (delete all objects)
166
+ deleted_count = await s3.empty_bucket('bucket')
167
+ ```
168
+
135
169
  ### http (deprecated)
136
170
 
137
171
  HTTP client helpers using [httpx](https://www.python-httpx.org/).
@@ -176,6 +210,65 @@ async with niquests.AsyncSession() as session:
176
210
  cache.get_database("db-id") # Specific database (id, title, properties, cached_at)
177
211
  ```
178
212
 
213
+ ### gh
214
+
215
+ GitHub API helpers using [niquests](https://github.com/jawah/niquests).
216
+
217
+ ```bash
218
+ uv add tracktolib[gh]
219
+ ```
220
+
221
+ ```python
222
+ from tracktolib.gh import GitHubClient
223
+
224
+ async with GitHubClient() as gh: # Uses GITHUB_TOKEN env var
225
+ # Issue comments
226
+ comments = await gh.get_issue_comments("owner/repo", 123)
227
+ await gh.create_issue_comment("owner/repo", 123, "Hello!")
228
+ await gh.delete_comments_with_marker("owner/repo", 123, "<!-- bot -->")
229
+
230
+ # Labels
231
+ labels = await gh.get_issue_labels("owner/repo", 123)
232
+ await gh.add_labels("owner/repo", 123, ["bug", "priority"])
233
+ await gh.remove_label("owner/repo", 123, "wontfix")
234
+
235
+ # Deployments
236
+ deploys = await gh.get_deployments("owner/repo", environment="production")
237
+ await gh.mark_deployment_inactive("owner/repo", "preview-123")
238
+ ```
239
+
240
+ ### cf
241
+
242
+ Cloudflare DNS API helpers using [niquests](https://github.com/jawah/niquests).
243
+
244
+ ```bash
245
+ uv add tracktolib[cf]
246
+ ```
247
+
248
+ ```python
249
+ from tracktolib.cf import CloudflareDNSClient
250
+
251
+ async with CloudflareDNSClient() as cf: # Uses CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID env vars
252
+ # Get a DNS record
253
+ record = await cf.get_dns_record("app.example.com", "CNAME")
254
+
255
+ # Create a DNS record
256
+ record = await cf.create_dns_record(
257
+ "app.example.com",
258
+ "target.example.com",
259
+ record_type="CNAME",
260
+ ttl=60,
261
+ proxied=True,
262
+ )
263
+
264
+ # Delete by ID or name
265
+ await cf.delete_dns_record(record["id"])
266
+ await cf.delete_dns_record_by_name("app.example.com", "CNAME")
267
+
268
+ # Check existence
269
+ exists = await cf.dns_record_exists("app.example.com")
270
+ ```
271
+
179
272
  ### tests
180
273
 
181
274
  Testing utilities using [deepdiff](https://github.com/seperman/deepdiff).
@@ -87,12 +87,42 @@ uv add tracktolib[s3-minio]
87
87
 
88
88
  ### s3-niquests
89
89
 
90
- Async S3 helpers using [niquests](https://github.com/jawah/niquests) and [botocore](https://github.com/boto/botocore).
90
+ Async S3 helpers using [niquests](https://github.com/jawah/niquests) and [botocore](https://github.com/boto/botocore) presigned URLs.
91
91
 
92
92
  ```bash
93
93
  uv add tracktolib[s3-niquests]
94
94
  ```
95
95
 
96
+ ```python
97
+ from tracktolib.s3.niquests import S3Session
98
+
99
+ async with S3Session(
100
+ endpoint_url='http://localhost:9000',
101
+ access_key='...',
102
+ secret_key='...',
103
+ region='us-east-1',
104
+ ) as s3:
105
+ # Object operations
106
+ await s3.put_object('bucket', 'path/file.txt', b'content')
107
+ content = await s3.get_object('bucket', 'path/file.txt')
108
+ await s3.delete_object('bucket', 'path/file.txt')
109
+
110
+ # Streaming upload (multipart for large files)
111
+ async def data_stream():
112
+ yield b'chunk1'
113
+ yield b'chunk2'
114
+ await s3.file_upload('bucket', 'large-file.bin', data_stream())
115
+
116
+ # Bucket policy management
117
+ policy = {'Version': '2012-10-17', 'Statement': [...]}
118
+ await s3.put_bucket_policy('bucket', policy)
119
+ await s3.get_bucket_policy('bucket')
120
+ await s3.delete_bucket_policy('bucket')
121
+
122
+ # Empty a bucket (delete all objects)
123
+ deleted_count = await s3.empty_bucket('bucket')
124
+ ```
125
+
96
126
  ### http (deprecated)
97
127
 
98
128
  HTTP client helpers using [httpx](https://www.python-httpx.org/).
@@ -137,6 +167,65 @@ async with niquests.AsyncSession() as session:
137
167
  cache.get_database("db-id") # Specific database (id, title, properties, cached_at)
138
168
  ```
139
169
 
170
+ ### gh
171
+
172
+ GitHub API helpers using [niquests](https://github.com/jawah/niquests).
173
+
174
+ ```bash
175
+ uv add tracktolib[gh]
176
+ ```
177
+
178
+ ```python
179
+ from tracktolib.gh import GitHubClient
180
+
181
+ async with GitHubClient() as gh: # Uses GITHUB_TOKEN env var
182
+ # Issue comments
183
+ comments = await gh.get_issue_comments("owner/repo", 123)
184
+ await gh.create_issue_comment("owner/repo", 123, "Hello!")
185
+ await gh.delete_comments_with_marker("owner/repo", 123, "<!-- bot -->")
186
+
187
+ # Labels
188
+ labels = await gh.get_issue_labels("owner/repo", 123)
189
+ await gh.add_labels("owner/repo", 123, ["bug", "priority"])
190
+ await gh.remove_label("owner/repo", 123, "wontfix")
191
+
192
+ # Deployments
193
+ deploys = await gh.get_deployments("owner/repo", environment="production")
194
+ await gh.mark_deployment_inactive("owner/repo", "preview-123")
195
+ ```
196
+
197
+ ### cf
198
+
199
+ Cloudflare DNS API helpers using [niquests](https://github.com/jawah/niquests).
200
+
201
+ ```bash
202
+ uv add tracktolib[cf]
203
+ ```
204
+
205
+ ```python
206
+ from tracktolib.cf import CloudflareDNSClient
207
+
208
+ async with CloudflareDNSClient() as cf: # Uses CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID env vars
209
+ # Get a DNS record
210
+ record = await cf.get_dns_record("app.example.com", "CNAME")
211
+
212
+ # Create a DNS record
213
+ record = await cf.create_dns_record(
214
+ "app.example.com",
215
+ "target.example.com",
216
+ record_type="CNAME",
217
+ ttl=60,
218
+ proxied=True,
219
+ )
220
+
221
+ # Delete by ID or name
222
+ await cf.delete_dns_record(record["id"])
223
+ await cf.delete_dns_record_by_name("app.example.com", "CNAME")
224
+
225
+ # Check existence
226
+ exists = await cf.dns_record_exists("app.example.com")
227
+ ```
228
+
140
229
  ### tests
141
230
 
142
231
  Testing utilities using [deepdiff](https://github.com/seperman/deepdiff).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tracktolib"
3
- version = "0.68.0"
3
+ version = "0.69.0"
4
4
  authors = [
5
5
  { email = "julien.brayere@tracktor.fr" }
6
6
  ]
@@ -46,11 +46,13 @@ pg = [
46
46
  "asyncpg>=0.27.0",
47
47
  "rich>=13.6.0",
48
48
  ]
49
- notion = [
50
- "niquests>=3.17.0"
51
- ]
49
+ notion = ["niquests>=3.17.0"]
50
+ gh = ["niquests>=3.17.0"]
51
+ cf = ["niquests>=3.17.0"]
52
52
 
53
53
  [dependency-groups]
54
+ niquests = ["niquests>=3.17.0"]
55
+
54
56
  dev = [
55
57
  "pytest-cov>=7.0.0,<8",
56
58
  "pytest>=9.0.1,<10",
@@ -66,9 +68,11 @@ dev = [
66
68
  "pycryptodome>=3.23.0",
67
69
  "aiobotocore==2.15.2",
68
70
  # niquests for s3-niquests tests (botocore comes from aiobotocore)
69
- "niquests>=3.17.0",
71
+ { include-group = "niquests" },
70
72
  "python-json-logger>=3.2.0",
71
73
  "prek>=0.2.30",
74
+ "datamodel-code-generator>=0.53.0",
75
+ "pytest-asyncio>=1.3.0",
72
76
  ]
73
77
 
74
78
  bump = [
@@ -98,6 +102,8 @@ filterwarnings = [
98
102
  "ignore::DeprecationWarning",
99
103
  "ignore::PendingDeprecationWarning"
100
104
  ]
105
+ asyncio_mode = "auto"
106
+ asyncio_default_fixture_loop_scope = "function"
101
107
 
102
108
  [tool.pyright]
103
109
  include = ["tracktolib", "tests"]
@@ -108,7 +114,7 @@ pythonPlatform = "Linux"
108
114
 
109
115
  [tool.commitizen]
110
116
  name = "cz_conventional_commits"
111
- version = "0.68.0"
117
+ version = "0.69.0"
112
118
  tag_format = "$version"
113
119
  version_files = [
114
120
  "pyproject.toml:version"
@@ -122,6 +128,9 @@ changelog_file = "CHANGELOG.md"
122
128
  line-length = 120
123
129
  target-version = "py314"
124
130
 
131
+ [tool.ruff.lint]
132
+ # I: isort - consolidates duplicate imports
133
+ extend-select = ["I"]
125
134
 
126
135
  [tool.ruff.lint.per-file-ignores]
127
136
  "__init__.py" = [
@@ -11,7 +11,6 @@ from typing import (
11
11
  Literal,
12
12
  Mapping,
13
13
  Sequence,
14
- Type,
15
14
  TypeAlias,
16
15
  TypedDict,
17
16
  get_args,
@@ -19,14 +18,14 @@ from typing import (
19
18
  get_type_hints,
20
19
  )
21
20
 
22
- from .utils import json_serial, get_first_line
21
+ from .utils import get_first_line, json_serial
23
22
 
24
23
  try:
25
- from fastapi import params, APIRouter
24
+ import starlette.status
25
+ from fastapi import APIRouter, params
26
26
  from fastapi.responses import JSONResponse
27
- from pydantic.alias_generators import to_camel
28
27
  from pydantic import BaseModel, ConfigDict
29
- import starlette.status
28
+ from pydantic.alias_generators import to_camel
30
29
  except ImportError:
31
30
  raise ImportError('Please install fastapi, pydantic or tracktolib with "api" to use this module')
32
31
 
@@ -61,7 +60,7 @@ class MethodMeta(TypedDict):
61
60
  status_code: StatusCode
62
61
  dependencies: Dependencies
63
62
  path: str | None
64
- response_model: Type[BaseModel | None | Sequence[BaseModel]] | None
63
+ response_model: type[BaseModel | None | Sequence[BaseModel]] | None
65
64
  openapi_extra: dict[str, Any] | None
66
65
  name: str | None
67
66
  summary: str | None
@@ -82,7 +81,7 @@ class Endpoint:
82
81
  status_code: StatusCode = None,
83
82
  dependencies: Dependencies = None,
84
83
  path: str | None = None,
85
- model: Type[B] | None = None,
84
+ model: type[B] | None = None,
86
85
  openapi_extra: dict[str, Any] | None = None,
87
86
  name: str | None = None,
88
87
  summary: str | None = None,
@@ -109,7 +108,7 @@ class Endpoint:
109
108
  status_code: StatusCode = None,
110
109
  dependencies: Dependencies = None,
111
110
  path: str | None = None,
112
- model: Type[B] | None = None,
111
+ model: type[B] | None = None,
113
112
  openapi_extra: dict[str, Any] | None = None,
114
113
  name: str | None = None,
115
114
  summary: str | None = None,
@@ -135,7 +134,7 @@ class Endpoint:
135
134
  status_code: StatusCode = None,
136
135
  dependencies: Dependencies = None,
137
136
  path: str | None = None,
138
- model: Type[B] | None = None,
137
+ model: type[B] | None = None,
139
138
  openapi_extra: dict[str, Any] | None = None,
140
139
  name: str | None = None,
141
140
  summary: str | None = None,
@@ -161,7 +160,7 @@ class Endpoint:
161
160
  status_code: StatusCode = None,
162
161
  dependencies: Dependencies = None,
163
162
  path: str | None = None,
164
- model: Type[B] | None = None,
163
+ model: type[B] | None = None,
165
164
  openapi_extra: dict[str, Any] | None = None,
166
165
  name: str | None = None,
167
166
  summary: str | None = None,
@@ -187,7 +186,7 @@ class Endpoint:
187
186
  status_code: StatusCode = None,
188
187
  dependencies: Dependencies = None,
189
188
  path: str | None = None,
190
- model: Type[B] | None = None,
189
+ model: type[B] | None = None,
191
190
  openapi_extra: dict[str, Any] | None = None,
192
191
  name: str | None = None,
193
192
  summary: str | None = None,
@@ -216,7 +215,7 @@ def _get_method_wrapper[B: _BaseModelBound](
216
215
  status_code: StatusCode = None,
217
216
  dependencies: Dependencies = None,
218
217
  path: str | None = None,
219
- model: Type[B] | None = None,
218
+ model: type[B] | None = None,
220
219
  openapi_extra: dict[str, Any] | None = None,
221
220
  name: str | None = None,
222
221
  summary: str | None = None,
@@ -335,7 +334,7 @@ def check_status(resp, status: int = starlette.status.HTTP_200_OK):
335
334
  raise AssertionError(json.dumps(resp.json(), indent=4))
336
335
 
337
336
 
338
- def generate_list_name_model[B: _BaseModelBound](model: Type[B], status: int | None = None) -> dict:
337
+ def generate_list_name_model[B: _BaseModelBound](model: type[B], status: int | None = None) -> dict:
339
338
  _status = "200" if status is None else str(status)
340
339
  if get_origin(model) and get_origin(model) is list:
341
340
  _title = f"Array[{get_args(model)[0].__name__}]"
@@ -0,0 +1,8 @@
1
+ from .client import CloudflareDNSClient, CloudflareError
2
+ from .types import DnsRecord
3
+
4
+ __all__ = [
5
+ "CloudflareDNSClient",
6
+ "CloudflareError",
7
+ "DnsRecord",
8
+ ]
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Any, cast
6
+
7
+ try:
8
+ import niquests
9
+ except ImportError:
10
+ raise ImportError('Please install niquests or tracktolib with "cf" to use this module')
11
+
12
+ if TYPE_CHECKING:
13
+ from urllib3.util.retry import Retry
14
+
15
+ from tracktolib.cf.types import DnsRecord
16
+
17
+
18
+ class CloudflareError(Exception):
19
+ """Error raised when a Cloudflare API call fails."""
20
+
21
+ def __init__(self, message: str, status_code: int | None = None, errors: list | None = None):
22
+ self.status_code = status_code
23
+ self.errors = errors or []
24
+ super().__init__(message)
25
+
26
+
27
+ @dataclass
28
+ class CloudflareDNSClient:
29
+ """
30
+ Async Cloudflare DNS API client for managing DNS records.
31
+
32
+ Requires CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID environment variables,
33
+ or pass them directly to the constructor.
34
+ """
35
+
36
+ zone_id: str | None = field(default_factory=lambda: os.environ.get("CLOUDFLARE_ZONE_ID"))
37
+ token: str | None = field(default_factory=lambda: os.environ.get("CLOUDFLARE_API_TOKEN"))
38
+ base_url: str = "https://api.cloudflare.com/client/v4"
39
+ retries: int | Retry = 0
40
+ hooks: Any = None
41
+ session: niquests.AsyncSession = field(init=False, repr=False)
42
+
43
+ def __post_init__(self) -> None:
44
+ if not self.token:
45
+ raise ValueError("CLOUDFLARE_API_TOKEN environment variable is required")
46
+ if not self.zone_id:
47
+ raise ValueError("CLOUDFLARE_ZONE_ID environment variable is required")
48
+
49
+ self.session = niquests.AsyncSession(
50
+ base_url=self.base_url,
51
+ retries=self.retries,
52
+ hooks=self.hooks,
53
+ headers={
54
+ "Authorization": f"Bearer {self.token}",
55
+ "Content-Type": "application/json",
56
+ },
57
+ )
58
+
59
+ async def __aenter__(self) -> "CloudflareDNSClient":
60
+ return self
61
+
62
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
63
+ await self.aclose()
64
+
65
+ async def aclose(self) -> None:
66
+ """Close the underlying session."""
67
+ await self.session.close()
68
+
69
+ def _handle_response(self, response: niquests.Response) -> dict:
70
+ """Handle Cloudflare API response and raise on errors."""
71
+ data = response.json()
72
+ if not data.get("success", False):
73
+ errors = data.get("errors", [])
74
+ error_messages = [e.get("message", str(e)) for e in errors]
75
+ raise CloudflareError(
76
+ f"Cloudflare API error: {', '.join(error_messages)}",
77
+ status_code=response.status_code,
78
+ errors=errors,
79
+ )
80
+ return data
81
+
82
+ # DNS Records
83
+
84
+ async def get_dns_record(self, name: str, record_type: str = "CNAME") -> DnsRecord | None:
85
+ """
86
+ Get a DNS record by name and type.
87
+
88
+ Returns None if the record doesn't exist.
89
+ """
90
+ params = {"type": record_type, "name": name}
91
+ response = await self.session.get(f"/zones/{self.zone_id}/dns_records", params=params)
92
+ data = self._handle_response(response)
93
+
94
+ results = data.get("result", [])
95
+ if not results:
96
+ return None
97
+
98
+ return cast("DnsRecord", results[0])
99
+
100
+ async def create_dns_record(
101
+ self,
102
+ name: str,
103
+ content: str,
104
+ record_type: str = "CNAME",
105
+ *,
106
+ ttl: int = 1,
107
+ proxied: bool = False,
108
+ comment: str | None = None,
109
+ ) -> DnsRecord:
110
+ """
111
+ Create a DNS record.
112
+
113
+ The ttl parameter defaults to 1 (automatic). Set to a value between 60-86400 for manual TTL.
114
+ """
115
+ payload: dict = {
116
+ "type": record_type,
117
+ "name": name,
118
+ "content": content,
119
+ "ttl": ttl,
120
+ "proxied": proxied,
121
+ }
122
+ if comment:
123
+ payload["comment"] = comment
124
+
125
+ response = await self.session.post(f"/zones/{self.zone_id}/dns_records", json=payload)
126
+ data = self._handle_response(response)
127
+ return cast("DnsRecord", data["result"])
128
+
129
+ async def delete_dns_record(self, record_id: str) -> None:
130
+ """Delete a DNS record by ID."""
131
+ response = await self.session.delete(f"/zones/{self.zone_id}/dns_records/{record_id}")
132
+ self._handle_response(response)
133
+
134
+ async def delete_dns_record_by_name(self, name: str, record_type: str = "CNAME") -> bool:
135
+ """
136
+ Delete a DNS record by name and type.
137
+
138
+ Returns True if deleted, False if the record didn't exist.
139
+ """
140
+ record = await self.get_dns_record(name, record_type)
141
+ if record is None:
142
+ return False
143
+
144
+ await self.delete_dns_record(record["id"])
145
+ return True
146
+
147
+ async def dns_record_exists(self, name: str, record_type: str = "CNAME") -> bool:
148
+ """Check if a DNS record exists."""
149
+ return await self.get_dns_record(name, record_type) is not None
@@ -0,0 +1,17 @@
1
+ from typing import NotRequired, TypedDict
2
+
3
+
4
+ class DnsRecord(TypedDict):
5
+ """Cloudflare DNS record response."""
6
+
7
+ id: str
8
+ name: str
9
+ type: str
10
+ content: str
11
+ ttl: int
12
+ proxied: bool
13
+ proxiable: NotRequired[bool]
14
+ created_on: NotRequired[str]
15
+ modified_on: NotRequired[str]
16
+ comment: NotRequired[str]
17
+ tags: NotRequired[list[str]]
@@ -0,0 +1,11 @@
1
+ from .client import GitHubClient, ProgressCallback
2
+ from .types import Deployment, DeploymentStatus, IssueComment, Label
3
+
4
+ __all__ = [
5
+ "Deployment",
6
+ "DeploymentStatus",
7
+ "GitHubClient",
8
+ "IssueComment",
9
+ "Label",
10
+ "ProgressCallback",
11
+ ]