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.
- {tracktolib-0.68.0 → tracktolib-0.69.0}/PKG-INFO +95 -2
- {tracktolib-0.68.0 → tracktolib-0.69.0}/README.md +90 -1
- {tracktolib-0.68.0 → tracktolib-0.69.0}/pyproject.toml +15 -6
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/api.py +12 -13
- tracktolib-0.69.0/tracktolib/cf/__init__.py +8 -0
- tracktolib-0.69.0/tracktolib/cf/client.py +149 -0
- tracktolib-0.69.0/tracktolib/cf/types.py +17 -0
- tracktolib-0.69.0/tracktolib/gh/__init__.py +11 -0
- tracktolib-0.69.0/tracktolib/gh/client.py +206 -0
- tracktolib-0.69.0/tracktolib/gh/types.py +203 -0
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/http_utils.py +1 -1
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/logs.py +1 -1
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/models.py +0 -1
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg/__init__.py +10 -10
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg/query.py +1 -1
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg/utils.py +5 -5
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg_sync.py +3 -5
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/pg_utils.py +1 -4
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/s3/niquests.py +235 -32
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/s3/s3.py +1 -1
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/utils.py +12 -3
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/__init__.py +0 -0
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/__init__.py +0 -0
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/blocks.py +0 -0
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/cache.py +0 -0
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/fetch.py +0 -0
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/markdown.py +3 -3
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/notion/utils.py +5 -5
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/s3/__init__.py +0 -0
- {tracktolib-0.68.0 → tracktolib-0.69.0}/tracktolib/s3/minio.py +1 -1
- {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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
21
|
+
from .utils import get_first_line, json_serial
|
|
23
22
|
|
|
24
23
|
try:
|
|
25
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,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]]
|