okrlinkhub-sync-indicators 0.3.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.
- okrlinkhub_sync_indicators-0.3.0/.gitignore +5 -0
- okrlinkhub_sync_indicators-0.3.0/PKG-INFO +18 -0
- okrlinkhub_sync_indicators-0.3.0/README.md +5 -0
- okrlinkhub_sync_indicators-0.3.0/pyproject.toml +19 -0
- okrlinkhub_sync_indicators-0.3.0/src/okrlinkhub_sync_indicators/__init__.py +23 -0
- okrlinkhub_sync_indicators-0.3.0/src/okrlinkhub_sync_indicators/sdk.py +502 -0
- okrlinkhub_sync_indicators-0.3.0/tests/test_sdk.py +61 -0
- okrlinkhub_sync_indicators-0.3.0/uv.lock +330 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: okrlinkhub-sync-indicators
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Producer SDK for signed LinkHub Sync Indicators catalogs and callbacks.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: cryptography>=42
|
|
8
|
+
Provides-Extra: r2
|
|
9
|
+
Requires-Dist: boto3>=1.34; extra == 'r2'
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# okrlinkhub-sync-indicators
|
|
15
|
+
|
|
16
|
+
Producer SDK for generating signed catalogs from dbt manifests, publishing
|
|
17
|
+
them atomically to S3-compatible storage, and sending active metric values to
|
|
18
|
+
LinkHub.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "okrlinkhub-sync-indicators"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Producer SDK for signed LinkHub Sync Indicators catalogs and callbacks."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
dependencies = ["cryptography>=42"]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
r2 = ["boto3>=1.34"]
|
|
16
|
+
test = ["pytest>=8"]
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["src/okrlinkhub_sync_indicators"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .sdk import (
|
|
2
|
+
build_catalog_from_manifest,
|
|
3
|
+
build_plans_batches,
|
|
4
|
+
build_results_batches,
|
|
5
|
+
canonical_json,
|
|
6
|
+
emit_configured_results,
|
|
7
|
+
fetch_producer_config,
|
|
8
|
+
publish_catalog,
|
|
9
|
+
refresh_catalog,
|
|
10
|
+
sign_producer_request,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"build_catalog_from_manifest",
|
|
15
|
+
"build_plans_batches",
|
|
16
|
+
"build_results_batches",
|
|
17
|
+
"canonical_json",
|
|
18
|
+
"emit_configured_results",
|
|
19
|
+
"fetch_producer_config",
|
|
20
|
+
"publish_catalog",
|
|
21
|
+
"refresh_catalog",
|
|
22
|
+
"sign_producer_request",
|
|
23
|
+
]
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import re
|
|
9
|
+
import struct
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.request
|
|
12
|
+
from collections.abc import Iterable, Mapping
|
|
13
|
+
from datetime import UTC, date, datetime
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from cryptography.hazmat.primitives import serialization
|
|
17
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
18
|
+
|
|
19
|
+
CATALOG_SCHEMA = "sync-indicators.catalog/v2"
|
|
20
|
+
CATALOG_ENVELOPE_SCHEMA = "sync-indicators.catalog-envelope/v1"
|
|
21
|
+
CATALOG_POINTER_SCHEMA = "sync-indicators.catalog-pointer/v1"
|
|
22
|
+
RESULTS_SCHEMA = "sync-indicators.results/v1"
|
|
23
|
+
PLANS_SCHEMA = "sync-indicators.plans/v1"
|
|
24
|
+
PERIODICITIES = {"weekly", "monthly", "quarterly", "semesterly", "yearly"}
|
|
25
|
+
AGGREGATION_PERIODS = {"mtd", "qtd", "ytd", "lsm", "ltm"}
|
|
26
|
+
RELATION_PATTERN = re.compile(
|
|
27
|
+
r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?$"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def canonical_json(value: Any) -> bytes:
|
|
32
|
+
return json.dumps(
|
|
33
|
+
value,
|
|
34
|
+
ensure_ascii=False,
|
|
35
|
+
allow_nan=False,
|
|
36
|
+
separators=(",", ":"),
|
|
37
|
+
sort_keys=True,
|
|
38
|
+
).encode("utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _iso_datetime(value: datetime | str) -> str:
|
|
42
|
+
parsed = value if isinstance(value, datetime) else datetime.fromisoformat(value)
|
|
43
|
+
if parsed.tzinfo is None:
|
|
44
|
+
raise ValueError("Datetime values must include a timezone")
|
|
45
|
+
return parsed.astimezone(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _sync_meta(resource: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
49
|
+
meta = resource.get("meta")
|
|
50
|
+
if not isinstance(meta, Mapping):
|
|
51
|
+
return {}
|
|
52
|
+
sync = meta.get("sync_indicators")
|
|
53
|
+
return sync if isinstance(sync, Mapping) else {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_catalog_from_manifest(
|
|
57
|
+
manifest: Mapping[str, Any], *, generated_at: datetime | str | None = None
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
nodes = {
|
|
60
|
+
**(manifest.get("nodes") if isinstance(manifest.get("nodes"), Mapping) else {}),
|
|
61
|
+
**(manifest.get("sources") if isinstance(manifest.get("sources"), Mapping) else {}),
|
|
62
|
+
}
|
|
63
|
+
parent_map = manifest.get("parent_map")
|
|
64
|
+
if not isinstance(parent_map, Mapping):
|
|
65
|
+
raise ValueError("dbt manifest is missing parent_map")
|
|
66
|
+
|
|
67
|
+
def source_tables(unique_id: str) -> list[str]:
|
|
68
|
+
found: set[str] = set()
|
|
69
|
+
visited: set[str] = set()
|
|
70
|
+
|
|
71
|
+
def visit(node_id: str) -> None:
|
|
72
|
+
if node_id in visited:
|
|
73
|
+
return
|
|
74
|
+
visited.add(node_id)
|
|
75
|
+
node = nodes.get(node_id)
|
|
76
|
+
if isinstance(node, Mapping):
|
|
77
|
+
marker = _sync_meta(node).get("sourceTable")
|
|
78
|
+
if isinstance(marker, str) and marker:
|
|
79
|
+
found.add(marker)
|
|
80
|
+
parents = parent_map.get(node_id, [])
|
|
81
|
+
if isinstance(parents, list):
|
|
82
|
+
for parent in parents:
|
|
83
|
+
if isinstance(parent, str):
|
|
84
|
+
visit(parent)
|
|
85
|
+
|
|
86
|
+
visit(unique_id)
|
|
87
|
+
return sorted(found)
|
|
88
|
+
|
|
89
|
+
namespace: str | None = None
|
|
90
|
+
indicators: list[dict[str, Any]] = []
|
|
91
|
+
for unique_id, resource in nodes.items():
|
|
92
|
+
if not isinstance(unique_id, str) or not isinstance(resource, Mapping):
|
|
93
|
+
continue
|
|
94
|
+
sync = _sync_meta(resource)
|
|
95
|
+
definitions = sync.get("indicators")
|
|
96
|
+
if definitions is None:
|
|
97
|
+
continue
|
|
98
|
+
if not isinstance(definitions, list) or not definitions:
|
|
99
|
+
raise ValueError(f"{unique_id}: indicators must be a non-empty list")
|
|
100
|
+
node_namespace = sync.get("namespace")
|
|
101
|
+
source_key = sync.get("sourceKey")
|
|
102
|
+
dataset_key = sync.get("datasetKey")
|
|
103
|
+
if not all(isinstance(value, str) and value for value in (node_namespace, source_key, dataset_key)):
|
|
104
|
+
raise ValueError(f"{unique_id}: namespace, sourceKey and datasetKey are required")
|
|
105
|
+
if namespace is None:
|
|
106
|
+
namespace = str(node_namespace)
|
|
107
|
+
elif namespace != node_namespace:
|
|
108
|
+
raise ValueError("All Sync Indicators metadata must use one namespace")
|
|
109
|
+
tables = source_tables(unique_id)
|
|
110
|
+
if not tables:
|
|
111
|
+
raise ValueError(f"{unique_id}: no sourceTable metadata found in dbt lineage")
|
|
112
|
+
for definition in definitions:
|
|
113
|
+
if not isinstance(definition, Mapping):
|
|
114
|
+
raise ValueError(f"{unique_id}: invalid indicator definition")
|
|
115
|
+
required = ("indicatorKey", "metricKey", "description", "symbol", "periodicity", "isReverse")
|
|
116
|
+
if any(key not in definition for key in required):
|
|
117
|
+
raise ValueError(f"{unique_id}: indicator is missing required fields")
|
|
118
|
+
if definition["periodicity"] not in PERIODICITIES or not isinstance(definition["isReverse"], bool):
|
|
119
|
+
raise ValueError(f"{unique_id}: invalid indicator fields")
|
|
120
|
+
aggregation = definition.get("aggregationPeriod")
|
|
121
|
+
if aggregation is not None and aggregation not in AGGREGATION_PERIODS:
|
|
122
|
+
raise ValueError(f"{unique_id}: invalid aggregationPeriod")
|
|
123
|
+
period_offset = definition.get("periodOffset")
|
|
124
|
+
if period_offset is not None and period_offset != "previous_period":
|
|
125
|
+
raise ValueError(f"{unique_id}: invalid periodOffset")
|
|
126
|
+
plan_kinds = definition.get("planKinds", ["forecast", "target"])
|
|
127
|
+
if (
|
|
128
|
+
not isinstance(plan_kinds, list)
|
|
129
|
+
or not plan_kinds
|
|
130
|
+
or any(kind not in {"forecast", "target"} for kind in plan_kinds)
|
|
131
|
+
or len(plan_kinds) != len(set(plan_kinds))
|
|
132
|
+
):
|
|
133
|
+
raise ValueError(f"{unique_id}: invalid planKinds")
|
|
134
|
+
indicator = {key: definition[key] for key in required}
|
|
135
|
+
if aggregation is not None:
|
|
136
|
+
indicator["aggregationPeriod"] = aggregation
|
|
137
|
+
if period_offset is not None:
|
|
138
|
+
indicator["periodOffset"] = period_offset
|
|
139
|
+
indicator["planKinds"] = plan_kinds
|
|
140
|
+
indicator["discovery"] = {
|
|
141
|
+
"sourceKey": source_key,
|
|
142
|
+
"datasetKey": dataset_key,
|
|
143
|
+
"tableNames": tables,
|
|
144
|
+
}
|
|
145
|
+
indicators.append(indicator)
|
|
146
|
+
if not namespace or not indicators:
|
|
147
|
+
raise ValueError("dbt manifest contains no Sync Indicators metadata")
|
|
148
|
+
indicators.sort(key=lambda item: item["indicatorKey"])
|
|
149
|
+
keys = [item["indicatorKey"] for item in indicators]
|
|
150
|
+
pairs = [(item["indicatorKey"], item["metricKey"]) for item in indicators]
|
|
151
|
+
if len(keys) != len(set(keys)) or len(pairs) != len(set(pairs)):
|
|
152
|
+
raise ValueError("Catalog indicator keys must be unique")
|
|
153
|
+
definition = {"namespace": namespace, "indicators": indicators}
|
|
154
|
+
definition_sha = hashlib.sha256(canonical_json(definition)).hexdigest()
|
|
155
|
+
return {
|
|
156
|
+
"schema": CATALOG_SCHEMA,
|
|
157
|
+
"namespace": namespace,
|
|
158
|
+
"catalogVersion": f"sha256-{definition_sha}",
|
|
159
|
+
"generatedAt": _iso_datetime(generated_at or datetime.now(UTC)),
|
|
160
|
+
"indicators": indicators,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _load_private_key(private_key: str | bytes) -> Ed25519PrivateKey:
|
|
165
|
+
raw = private_key.encode() if isinstance(private_key, str) else private_key
|
|
166
|
+
if b"BEGIN PRIVATE KEY" in raw:
|
|
167
|
+
loaded = serialization.load_pem_private_key(raw, password=None)
|
|
168
|
+
if not isinstance(loaded, Ed25519PrivateKey):
|
|
169
|
+
raise ValueError("Catalog signing key must be Ed25519")
|
|
170
|
+
return loaded
|
|
171
|
+
decoded = base64.b64decode(raw, validate=True)
|
|
172
|
+
if len(decoded) != 32:
|
|
173
|
+
raise ValueError("Raw Ed25519 private key must contain 32 bytes")
|
|
174
|
+
return Ed25519PrivateKey.from_private_bytes(decoded)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def publish_catalog(
|
|
178
|
+
s3_client: Any,
|
|
179
|
+
*,
|
|
180
|
+
bucket: str,
|
|
181
|
+
pointer_key: str,
|
|
182
|
+
catalog: Mapping[str, Any],
|
|
183
|
+
private_key: str | bytes,
|
|
184
|
+
key_id: str,
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
catalog_body = canonical_json(catalog)
|
|
187
|
+
catalog_sha = hashlib.sha256(catalog_body).hexdigest()
|
|
188
|
+
signature = base64.b64encode(_load_private_key(private_key).sign(catalog_body)).decode("ascii")
|
|
189
|
+
definition_sha = str(catalog["catalogVersion"]).removeprefix("sha256-")
|
|
190
|
+
prefix = pointer_key.rsplit("/", 1)[0] if "/" in pointer_key else ""
|
|
191
|
+
object_key = f"{prefix + '/' if prefix else ''}versions/{definition_sha}.json"
|
|
192
|
+
envelope = {
|
|
193
|
+
"schema": CATALOG_ENVELOPE_SCHEMA,
|
|
194
|
+
"keyId": key_id,
|
|
195
|
+
"catalogSha256": catalog_sha,
|
|
196
|
+
"signature": signature,
|
|
197
|
+
"catalog": dict(catalog),
|
|
198
|
+
}
|
|
199
|
+
envelope_body = canonical_json(envelope)
|
|
200
|
+
envelope_sha = hashlib.sha256(envelope_body).hexdigest()
|
|
201
|
+
pointer = {
|
|
202
|
+
"schema": CATALOG_POINTER_SCHEMA,
|
|
203
|
+
"catalogVersion": catalog["catalogVersion"],
|
|
204
|
+
"definitionSha256": definition_sha,
|
|
205
|
+
"objectKey": object_key,
|
|
206
|
+
"envelopeSha256": envelope_sha,
|
|
207
|
+
}
|
|
208
|
+
try:
|
|
209
|
+
existing = json.loads(s3_client.get_object(Bucket=bucket, Key=pointer_key)["Body"].read())
|
|
210
|
+
if isinstance(existing, dict) and existing.get("definitionSha256") == definition_sha:
|
|
211
|
+
return {**pointer, "published": False}
|
|
212
|
+
except KeyError:
|
|
213
|
+
pass
|
|
214
|
+
except Exception as error:
|
|
215
|
+
response = getattr(error, "response", {})
|
|
216
|
+
code = str(response.get("Error", {}).get("Code", "")) if isinstance(response, dict) else ""
|
|
217
|
+
if code not in {"NoSuchKey", "NotFound", "404"}:
|
|
218
|
+
raise
|
|
219
|
+
s3_client.put_object(
|
|
220
|
+
Bucket=bucket,
|
|
221
|
+
Key=object_key,
|
|
222
|
+
Body=envelope_body,
|
|
223
|
+
ContentType="application/json",
|
|
224
|
+
ChecksumSHA256=base64.b64encode(hashlib.sha256(envelope_body).digest()).decode("ascii"),
|
|
225
|
+
)
|
|
226
|
+
s3_client.put_object(
|
|
227
|
+
Bucket=bucket,
|
|
228
|
+
Key=pointer_key,
|
|
229
|
+
Body=canonical_json(pointer),
|
|
230
|
+
ContentType="application/json",
|
|
231
|
+
)
|
|
232
|
+
return {**pointer, "published": True}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def sign_producer_request(*, timestamp: str, path: str, body: bytes, secret: str) -> str:
|
|
236
|
+
message = timestamp.encode() + b".POST." + path.encode() + b"." + body
|
|
237
|
+
return base64.b64encode(hmac.new(secret.encode(), message, hashlib.sha256).digest()).decode("ascii")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _producer_post(callback_url: str, path: str, *, target_id: str, secret: str) -> dict[str, Any]:
|
|
241
|
+
body = canonical_json({"targetId": target_id})
|
|
242
|
+
timestamp = _iso_datetime(datetime.now(UTC))
|
|
243
|
+
base_url = callback_url.removesuffix("/results").removesuffix("/plans")
|
|
244
|
+
request = urllib.request.Request(
|
|
245
|
+
f"{base_url}{path.removeprefix('/sync-indicators')}",
|
|
246
|
+
data=body,
|
|
247
|
+
method="POST",
|
|
248
|
+
headers={
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
"X-Sync-Timestamp": timestamp,
|
|
251
|
+
"X-Sync-Signature": sign_producer_request(
|
|
252
|
+
timestamp=timestamp, path=path, body=body, secret=secret
|
|
253
|
+
),
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
try:
|
|
257
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
258
|
+
payload = response.read()
|
|
259
|
+
except urllib.error.HTTPError as error:
|
|
260
|
+
detail = error.read().decode("utf-8", errors="replace")[:500]
|
|
261
|
+
raise RuntimeError(f"Sync Indicators producer request failed ({error.code}): {detail}") from error
|
|
262
|
+
parsed = json.loads(payload or b"{}")
|
|
263
|
+
if not isinstance(parsed, dict):
|
|
264
|
+
raise TypeError("Sync Indicators producer response must be an object")
|
|
265
|
+
return parsed
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def refresh_catalog(callback_url: str, *, target_id: str, secret: str) -> dict[str, Any]:
|
|
269
|
+
return _producer_post(
|
|
270
|
+
callback_url,
|
|
271
|
+
"/sync-indicators/catalog/refresh",
|
|
272
|
+
target_id=target_id,
|
|
273
|
+
secret=secret,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def fetch_producer_config(callback_url: str, *, target_id: str, secret: str) -> dict[str, Any]:
|
|
278
|
+
result = _producer_post(
|
|
279
|
+
callback_url,
|
|
280
|
+
"/sync-indicators/config",
|
|
281
|
+
target_id=target_id,
|
|
282
|
+
secret=secret,
|
|
283
|
+
)
|
|
284
|
+
if not isinstance(result.get("catalogVersion"), str) or not isinstance(result.get("activeMetrics"), list):
|
|
285
|
+
raise ValueError("Invalid Sync Indicators producer config")
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _normalize_value(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
290
|
+
value_date = value.get("date")
|
|
291
|
+
if isinstance(value_date, date):
|
|
292
|
+
value_date = value_date.isoformat()
|
|
293
|
+
amount = value.get("value")
|
|
294
|
+
if (
|
|
295
|
+
not isinstance(value.get("indicatorKey"), str)
|
|
296
|
+
or not isinstance(value.get("metricKey"), str)
|
|
297
|
+
or not isinstance(value_date, str)
|
|
298
|
+
or not isinstance(amount, (int, float))
|
|
299
|
+
or isinstance(amount, bool)
|
|
300
|
+
or not math.isfinite(float(amount))
|
|
301
|
+
):
|
|
302
|
+
raise ValueError("Invalid result value")
|
|
303
|
+
date.fromisoformat(value_date)
|
|
304
|
+
return {"indicatorKey": value["indicatorKey"], "metricKey": value["metricKey"], "date": value_date, "value": amount}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def build_results_batches(
|
|
308
|
+
*, target_id: str, catalog_version: str, source_run_id: str,
|
|
309
|
+
timestamp: datetime | str, values: Iterable[Mapping[str, Any]],
|
|
310
|
+
scope: Iterable[Mapping[str, Any]], batch_size: int = 500,
|
|
311
|
+
) -> list[dict[str, Any]]:
|
|
312
|
+
if not 1 <= batch_size <= 500:
|
|
313
|
+
raise ValueError("batch_size must be between 1 and 500")
|
|
314
|
+
normalized = sorted((_normalize_value(value) for value in values), key=lambda item: (item["indicatorKey"], item["metricKey"], item["date"]))
|
|
315
|
+
value_keys = [
|
|
316
|
+
(item["indicatorKey"], item["metricKey"], item["date"])
|
|
317
|
+
for item in normalized
|
|
318
|
+
]
|
|
319
|
+
if len(value_keys) != len(set(value_keys)):
|
|
320
|
+
raise ValueError("Duplicate Sync Indicators result value")
|
|
321
|
+
normalized_scope = sorted(
|
|
322
|
+
({"indicatorKey": str(item["indicatorKey"]), "metricKey": str(item["metricKey"])} for item in scope),
|
|
323
|
+
key=lambda item: (item["indicatorKey"], item["metricKey"]),
|
|
324
|
+
)
|
|
325
|
+
if not normalized_scope or len(normalized_scope) > 500:
|
|
326
|
+
raise ValueError("Invalid result scope")
|
|
327
|
+
allowed = {(item["indicatorKey"], item["metricKey"]) for item in normalized_scope}
|
|
328
|
+
if any((item["indicatorKey"], item["metricKey"]) not in allowed for item in normalized):
|
|
329
|
+
raise ValueError("Result value is outside snapshot scope")
|
|
330
|
+
snapshot_hash = hashlib.sha256(canonical_json({
|
|
331
|
+
"scope": normalized_scope,
|
|
332
|
+
"values": [{
|
|
333
|
+
"indicatorKey": item["indicatorKey"], "metricKey": item["metricKey"],
|
|
334
|
+
"date": item["date"], "valueFloat64Hex": struct.pack(">d", float(item["value"])).hex(),
|
|
335
|
+
} for item in normalized],
|
|
336
|
+
})).hexdigest()
|
|
337
|
+
batch_count = max(1, math.ceil(len(normalized) / batch_size))
|
|
338
|
+
emitted_at = _iso_datetime(timestamp)
|
|
339
|
+
return [{
|
|
340
|
+
"schema": RESULTS_SCHEMA, "targetId": target_id, "catalogVersion": catalog_version,
|
|
341
|
+
"sourceRunId": source_run_id, "idempotencyKey": f"{source_run_id}:{index}",
|
|
342
|
+
"timestamp": emitted_at, "values": normalized[index * batch_size:(index + 1) * batch_size],
|
|
343
|
+
"reconciliation": {"mode": "snapshot", "snapshotCreatedAt": emitted_at,
|
|
344
|
+
"scope": normalized_scope, "batchIndex": index, "batchCount": batch_count,
|
|
345
|
+
"totalValueCount": len(normalized), "snapshotSha256": snapshot_hash},
|
|
346
|
+
} for index in range(batch_count)]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def build_plans_batches(
|
|
350
|
+
*, target_id: str, catalog_version: str, source_run_id: str,
|
|
351
|
+
timestamp: datetime | str, plans: Iterable[Mapping[str, Any]],
|
|
352
|
+
scope: Iterable[Mapping[str, Any]], batch_size: int = 500,
|
|
353
|
+
) -> list[dict[str, Any]]:
|
|
354
|
+
if not 1 <= batch_size <= 500:
|
|
355
|
+
raise ValueError("batch_size must be between 1 and 500")
|
|
356
|
+
|
|
357
|
+
def normalize_plan(item: Mapping[str, Any]) -> dict[str, Any]:
|
|
358
|
+
point_date = item.get("date")
|
|
359
|
+
if isinstance(point_date, date):
|
|
360
|
+
point_date = point_date.isoformat()
|
|
361
|
+
amount = item.get("value")
|
|
362
|
+
if (
|
|
363
|
+
not isinstance(item.get("indicatorKey"), str)
|
|
364
|
+
or not item["indicatorKey"]
|
|
365
|
+
or not isinstance(item.get("metricKey"), str)
|
|
366
|
+
or not item["metricKey"]
|
|
367
|
+
or not isinstance(point_date, str)
|
|
368
|
+
or len(point_date) != 10
|
|
369
|
+
or item.get("kind") not in {"forecast", "target"}
|
|
370
|
+
or not isinstance(amount, (int, float))
|
|
371
|
+
or isinstance(amount, bool)
|
|
372
|
+
or not math.isfinite(float(amount))
|
|
373
|
+
):
|
|
374
|
+
raise ValueError("Invalid Sync Indicators plan point")
|
|
375
|
+
date.fromisoformat(point_date)
|
|
376
|
+
return {
|
|
377
|
+
"indicatorKey": item["indicatorKey"],
|
|
378
|
+
"metricKey": item["metricKey"],
|
|
379
|
+
"date": point_date,
|
|
380
|
+
"kind": item["kind"],
|
|
381
|
+
"value": amount,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
normalized = sorted(
|
|
385
|
+
(normalize_plan(item) for item in plans),
|
|
386
|
+
key=lambda item: (
|
|
387
|
+
item["indicatorKey"],
|
|
388
|
+
item["metricKey"],
|
|
389
|
+
item["date"],
|
|
390
|
+
item["kind"],
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
point_keys = [
|
|
394
|
+
(item["indicatorKey"], item["metricKey"], item["date"], item["kind"])
|
|
395
|
+
for item in normalized
|
|
396
|
+
]
|
|
397
|
+
if len(point_keys) != len(set(point_keys)):
|
|
398
|
+
raise ValueError("Duplicate Sync Indicators plan point")
|
|
399
|
+
normalized_scope = sorted(({
|
|
400
|
+
"indicatorKey": str(item["indicatorKey"]), "metricKey": str(item["metricKey"]), "kind": str(item["kind"]),
|
|
401
|
+
} for item in scope), key=lambda item: (item["indicatorKey"], item["metricKey"], item["kind"]))
|
|
402
|
+
allowed = {(item["indicatorKey"], item["metricKey"], item["kind"]) for item in normalized_scope}
|
|
403
|
+
scope_keys = [
|
|
404
|
+
(item["indicatorKey"], item["metricKey"], item["kind"])
|
|
405
|
+
for item in normalized_scope
|
|
406
|
+
]
|
|
407
|
+
if (
|
|
408
|
+
not normalized_scope
|
|
409
|
+
or len(normalized_scope) > 500
|
|
410
|
+
or len(scope_keys) != len(set(scope_keys))
|
|
411
|
+
or any(
|
|
412
|
+
(item["indicatorKey"], item["metricKey"], item["kind"])
|
|
413
|
+
not in allowed
|
|
414
|
+
for item in normalized
|
|
415
|
+
)
|
|
416
|
+
):
|
|
417
|
+
raise ValueError("Invalid plan scope")
|
|
418
|
+
emitted_at = _iso_datetime(timestamp)
|
|
419
|
+
snapshot_hash = hashlib.sha256(
|
|
420
|
+
canonical_json(
|
|
421
|
+
{
|
|
422
|
+
"scope": normalized_scope,
|
|
423
|
+
"plans": [
|
|
424
|
+
{
|
|
425
|
+
"indicatorKey": item["indicatorKey"],
|
|
426
|
+
"metricKey": item["metricKey"],
|
|
427
|
+
"date": item["date"],
|
|
428
|
+
"kind": item["kind"],
|
|
429
|
+
"valueFloat64Hex": struct.pack(">d", float(item["value"])).hex(),
|
|
430
|
+
}
|
|
431
|
+
for item in normalized
|
|
432
|
+
],
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
).hexdigest()
|
|
436
|
+
batch_count = max(1, math.ceil(len(normalized) / batch_size))
|
|
437
|
+
return [{
|
|
438
|
+
"schema": PLANS_SCHEMA, "targetId": target_id, "catalogVersion": catalog_version,
|
|
439
|
+
"sourceRunId": source_run_id, "idempotencyKey": f"{source_run_id}:{index}",
|
|
440
|
+
"timestamp": emitted_at, "plans": normalized[index * batch_size:(index + 1) * batch_size],
|
|
441
|
+
"reconciliation": {"mode": "snapshot", "snapshotCreatedAt": emitted_at,
|
|
442
|
+
"scope": normalized_scope, "batchIndex": index, "batchCount": batch_count,
|
|
443
|
+
"totalPointCount": len(normalized), "snapshotSha256": snapshot_hash},
|
|
444
|
+
} for index in range(batch_count)]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _post_batch(url: str, batch: Mapping[str, Any], *, secret: str) -> dict[str, Any]:
|
|
448
|
+
body = canonical_json(batch)
|
|
449
|
+
timestamp = str(batch["timestamp"])
|
|
450
|
+
signature = base64.b64encode(hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).digest()).decode("ascii")
|
|
451
|
+
request = urllib.request.Request(url, data=body, method="POST", headers={
|
|
452
|
+
"Content-Type": "application/json", "X-Sync-Timestamp": timestamp, "X-Sync-Signature": signature,
|
|
453
|
+
})
|
|
454
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
455
|
+
parsed = json.loads(response.read() or b"{}")
|
|
456
|
+
if not isinstance(parsed, dict):
|
|
457
|
+
raise TypeError("Sync Indicators callback returned a non-object")
|
|
458
|
+
return parsed
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def emit_configured_results(client: Any, *, config: Mapping[str, Any], source_run_id: str) -> list[dict[str, Any]]:
|
|
462
|
+
required = ("companyId", "targetId", "callbackUrl", "hmacSecret", "resultsRelation")
|
|
463
|
+
if any(not isinstance(config.get(key), str) or not config[key] for key in required):
|
|
464
|
+
raise ValueError("Incomplete Sync Indicators callback config")
|
|
465
|
+
if not RELATION_PATTERN.fullmatch(str(config["resultsRelation"])):
|
|
466
|
+
raise ValueError("resultsRelation must be a ClickHouse identifier")
|
|
467
|
+
producer = fetch_producer_config(str(config["callbackUrl"]), target_id=str(config["targetId"]), secret=str(config["hmacSecret"]))
|
|
468
|
+
active = producer["activeMetrics"]
|
|
469
|
+
pairs = {(str(item["indicatorKey"]), str(item["metricKey"])) for item in active}
|
|
470
|
+
if not pairs:
|
|
471
|
+
return []
|
|
472
|
+
rows = client.query(
|
|
473
|
+
f"SELECT indicator_key, metric_key, toString(value_date), toFloat64(value) FROM {config['resultsRelation']} WHERE company_id = {{company_id:String}} ORDER BY indicator_key, metric_key, value_date",
|
|
474
|
+
parameters={"company_id": config["companyId"]},
|
|
475
|
+
).result_rows
|
|
476
|
+
values = [{"indicatorKey": str(a), "metricKey": str(b), "date": str(c), "value": float(d)} for a, b, c, d in rows if (str(a), str(b)) in pairs]
|
|
477
|
+
emitted_at = datetime.now(UTC)
|
|
478
|
+
result_batches = build_results_batches(
|
|
479
|
+
target_id=str(config["targetId"]), catalog_version=str(producer["catalogVersion"]),
|
|
480
|
+
source_run_id=source_run_id, timestamp=emitted_at, values=values,
|
|
481
|
+
scope=[{"indicatorKey": a, "metricKey": b} for a, b in sorted(pairs)],
|
|
482
|
+
)
|
|
483
|
+
responses = [_post_batch(str(config["callbackUrl"]), batch, secret=str(config["hmacSecret"])) for batch in result_batches]
|
|
484
|
+
plans_relation = config.get("plansRelation")
|
|
485
|
+
if not isinstance(plans_relation, str) or not plans_relation:
|
|
486
|
+
return responses
|
|
487
|
+
if not RELATION_PATTERN.fullmatch(plans_relation):
|
|
488
|
+
raise ValueError("plansRelation must be a ClickHouse identifier")
|
|
489
|
+
plan_scope = {(str(item["indicatorKey"]), str(item["metricKey"]), str(kind)) for item in active for kind in item.get("planKinds", ["forecast", "target"])}
|
|
490
|
+
plan_rows = client.query(
|
|
491
|
+
f"SELECT indicator_key, metric_key, toString(plan_date), plan_kind, toFloat64(value) FROM {plans_relation} WHERE company_id = {{company_id:String}} ORDER BY indicator_key, metric_key, plan_date, plan_kind",
|
|
492
|
+
parameters={"company_id": config["companyId"]},
|
|
493
|
+
).result_rows
|
|
494
|
+
plans = [{"indicatorKey": str(a), "metricKey": str(b), "date": str(c), "kind": str(d), "value": float(e)} for a, b, c, d, e in plan_rows if (str(a), str(b), str(d)) in plan_scope]
|
|
495
|
+
plan_batches = build_plans_batches(
|
|
496
|
+
target_id=str(config["targetId"]), catalog_version=str(producer["catalogVersion"]),
|
|
497
|
+
source_run_id=source_run_id, timestamp=emitted_at, plans=plans,
|
|
498
|
+
scope=[{"indicatorKey": a, "metricKey": b, "kind": kind} for a, b, kind in sorted(plan_scope)],
|
|
499
|
+
)
|
|
500
|
+
plans_url = str(config.get("plansCallbackUrl") or str(config["callbackUrl"]).removesuffix("/results") + "/plans")
|
|
501
|
+
responses.extend(_post_batch(plans_url, batch, secret=str(config["hmacSecret"])) for batch in plan_batches)
|
|
502
|
+
return responses
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
8
|
+
|
|
9
|
+
from okrlinkhub_sync_indicators import build_catalog_from_manifest, canonical_json, publish_catalog
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
GOLDEN = json.loads(
|
|
13
|
+
(Path(__file__).resolve().parents[2] / "src" / "fixtures" / "catalog-v2-golden.json").read_text()
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def manifest():
|
|
18
|
+
return {
|
|
19
|
+
"nodes": {
|
|
20
|
+
"model.project.source": {"meta": {"sync_indicators": {"sourceTable": "tickets"}}},
|
|
21
|
+
"model.project.metric": {"meta": {"sync_indicators": {
|
|
22
|
+
"namespace": "pcg", "sourceKey": "abaddon-project", "datasetKey": "abaddon-project",
|
|
23
|
+
"indicators": [{"indicatorKey": "pcg.sla", "metricKey": "sla_pct", "description": "SLA", "symbol": "%", "periodicity": "monthly", "isReverse": False}],
|
|
24
|
+
}}},
|
|
25
|
+
},
|
|
26
|
+
"sources": {},
|
|
27
|
+
"parent_map": {"model.project.source": [], "model.project.metric": ["model.project.source"]},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_manifest_catalog_is_content_addressed():
|
|
32
|
+
catalog = build_catalog_from_manifest(manifest(), generated_at=datetime(2026, 8, 5, tzinfo=UTC))
|
|
33
|
+
assert catalog["catalogVersion"].startswith("sha256-")
|
|
34
|
+
assert catalog["indicators"][0]["discovery"]["tableNames"] == ["tickets"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_publish_writes_immutable_envelope_before_pointer():
|
|
38
|
+
writes = []
|
|
39
|
+
class S3:
|
|
40
|
+
def get_object(self, **_kwargs):
|
|
41
|
+
raise KeyError("missing")
|
|
42
|
+
def put_object(self, **kwargs):
|
|
43
|
+
writes.append(kwargs)
|
|
44
|
+
key = Ed25519PrivateKey.generate()
|
|
45
|
+
raw = key.private_bytes_raw()
|
|
46
|
+
catalog = build_catalog_from_manifest(manifest(), generated_at=datetime(2026, 8, 5, tzinfo=UTC))
|
|
47
|
+
receipt = publish_catalog(S3(), bucket="bucket", pointer_key="catalog/latest.json", catalog=catalog, private_key=base64.b64encode(raw).decode(), key_id="pcg-2026-08")
|
|
48
|
+
assert writes[0]["Key"].startswith("catalog/versions/")
|
|
49
|
+
assert writes[1]["Key"] == "catalog/latest.json"
|
|
50
|
+
pointer = json.loads(writes[1]["Body"])
|
|
51
|
+
assert pointer["envelopeSha256"] == hashlib.sha256(writes[0]["Body"]).hexdigest()
|
|
52
|
+
assert receipt["published"] is True
|
|
53
|
+
assert canonical_json(catalog)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_shared_catalog_v2_golden_vector():
|
|
57
|
+
catalog_body = canonical_json(GOLDEN["catalog"])
|
|
58
|
+
assert catalog_body.decode() == GOLDEN["canonicalCatalog"]
|
|
59
|
+
assert hashlib.sha256(catalog_body).hexdigest() == GOLDEN["catalogSha256"]
|
|
60
|
+
key = Ed25519PrivateKey.from_private_bytes(base64.b64decode(GOLDEN["privateKeyBase64"]))
|
|
61
|
+
assert base64.b64encode(key.sign(catalog_body)).decode() == GOLDEN["signature"]
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.11"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "boto3"
|
|
7
|
+
version = "1.43.64"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "botocore" },
|
|
11
|
+
{ name = "jmespath" },
|
|
12
|
+
{ name = "s3transfer" },
|
|
13
|
+
]
|
|
14
|
+
sdist = { url = "https://files.pythonhosted.org/packages/51/da/d5ca15f34a567f2d027df23395315983bd7ee35011e93c1cca12b7f89823/boto3-1.43.64.tar.gz", hash = "sha256:fc7522c3ed97d38176e0b1366406bca0fc8c888ae8d416bf953477b3f015a7b2", size = 112655, upload-time = "2026-08-04T20:12:42.45Z" }
|
|
15
|
+
wheels = [
|
|
16
|
+
{ url = "https://files.pythonhosted.org/packages/15/94/14cf3ec553edf78e5196289087ac52862c87ac179fa1cfdf1022ec366b79/boto3-1.43.64-py3-none-any.whl", hash = "sha256:2b555e63ece57cffb1ab1666fa6c23a0957e61f9d19ea8424d6c884124c3fd98", size = 140024, upload-time = "2026-08-04T20:12:41.147Z" },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[[package]]
|
|
20
|
+
name = "botocore"
|
|
21
|
+
version = "1.43.64"
|
|
22
|
+
source = { registry = "https://pypi.org/simple" }
|
|
23
|
+
dependencies = [
|
|
24
|
+
{ name = "jmespath" },
|
|
25
|
+
{ name = "python-dateutil" },
|
|
26
|
+
{ name = "urllib3" },
|
|
27
|
+
]
|
|
28
|
+
sdist = { url = "https://files.pythonhosted.org/packages/3b/67/0e39203c5c67f750874478cf89124486044580ec5cb391eec8eb13cf25b8/botocore-1.43.64.tar.gz", hash = "sha256:a2bb131f48111094fae0a2c896b42593797e935d9c22abb2ebae4290c6dbb5ea", size = 15842274, upload-time = "2026-08-04T20:12:31.598Z" }
|
|
29
|
+
wheels = [
|
|
30
|
+
{ url = "https://files.pythonhosted.org/packages/3d/d1/f27d1a4fb571e102828a4672cb3d19e0409275eb92c701b67a85879c0b04/botocore-1.43.64-py3-none-any.whl", hash = "sha256:f0a01c47d631ab95589c244566bca971294724b21862de1c12c7c3b0de236272", size = 15525459, upload-time = "2026-08-04T20:12:28.58Z" },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[[package]]
|
|
34
|
+
name = "cffi"
|
|
35
|
+
version = "2.1.1"
|
|
36
|
+
source = { registry = "https://pypi.org/simple" }
|
|
37
|
+
dependencies = [
|
|
38
|
+
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
|
39
|
+
]
|
|
40
|
+
sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
|
|
41
|
+
wheels = [
|
|
42
|
+
{ url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" },
|
|
43
|
+
{ url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" },
|
|
44
|
+
{ url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" },
|
|
45
|
+
{ url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" },
|
|
46
|
+
{ url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" },
|
|
47
|
+
{ url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" },
|
|
48
|
+
{ url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" },
|
|
49
|
+
{ url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" },
|
|
50
|
+
{ url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" },
|
|
51
|
+
{ url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" },
|
|
52
|
+
{ url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" },
|
|
53
|
+
{ url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" },
|
|
54
|
+
{ url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" },
|
|
55
|
+
{ url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" },
|
|
56
|
+
{ url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" },
|
|
57
|
+
{ url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" },
|
|
58
|
+
{ url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" },
|
|
59
|
+
{ url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" },
|
|
60
|
+
{ url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" },
|
|
61
|
+
{ url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" },
|
|
62
|
+
{ url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" },
|
|
63
|
+
{ url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" },
|
|
64
|
+
{ url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
|
|
65
|
+
{ url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
|
|
66
|
+
{ url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
|
|
67
|
+
{ url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
|
|
69
|
+
{ url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
|
|
70
|
+
{ url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
|
|
71
|
+
{ url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
|
|
72
|
+
{ url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
|
|
73
|
+
{ url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
|
|
74
|
+
{ url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
|
|
75
|
+
{ url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
|
|
76
|
+
{ url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
|
|
77
|
+
{ url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
|
|
78
|
+
{ url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
|
|
79
|
+
{ url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
|
|
80
|
+
{ url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
|
|
81
|
+
{ url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
|
|
82
|
+
{ url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
|
|
83
|
+
{ url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
|
|
84
|
+
{ url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
|
|
85
|
+
{ url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
|
|
86
|
+
{ url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
|
|
87
|
+
{ url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
|
|
88
|
+
{ url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
|
|
89
|
+
{ url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
|
|
90
|
+
{ url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
|
|
91
|
+
{ url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
|
|
92
|
+
{ url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
|
|
93
|
+
{ url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
|
|
94
|
+
{ url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
|
|
95
|
+
{ url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
|
|
96
|
+
{ url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
|
|
97
|
+
{ url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
|
|
98
|
+
{ url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
|
|
99
|
+
{ url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
|
|
100
|
+
{ url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
|
|
101
|
+
{ url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
|
|
102
|
+
{ url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
|
|
103
|
+
{ url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
|
|
104
|
+
{ url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
|
|
105
|
+
{ url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
|
|
106
|
+
{ url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
|
|
107
|
+
{ url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
|
|
108
|
+
{ url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
|
|
109
|
+
{ url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
|
|
110
|
+
{ url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
|
|
111
|
+
{ url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
|
|
112
|
+
{ url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
|
|
113
|
+
{ url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
|
|
114
|
+
{ url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
|
|
115
|
+
{ url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
|
|
116
|
+
{ url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
|
|
117
|
+
{ url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
|
|
118
|
+
{ url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
|
|
119
|
+
{ url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
|
|
120
|
+
{ url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
|
|
121
|
+
{ url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
|
|
122
|
+
{ url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
|
|
123
|
+
{ url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
|
|
124
|
+
{ url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
|
|
125
|
+
{ url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
|
|
126
|
+
{ url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
|
|
127
|
+
{ url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
|
|
128
|
+
{ url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
[[package]]
|
|
132
|
+
name = "colorama"
|
|
133
|
+
version = "0.4.6"
|
|
134
|
+
source = { registry = "https://pypi.org/simple" }
|
|
135
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
136
|
+
wheels = [
|
|
137
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
[[package]]
|
|
141
|
+
name = "cryptography"
|
|
142
|
+
version = "50.0.0"
|
|
143
|
+
source = { registry = "https://pypi.org/simple" }
|
|
144
|
+
dependencies = [
|
|
145
|
+
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
|
146
|
+
]
|
|
147
|
+
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
|
|
148
|
+
wheels = [
|
|
149
|
+
{ url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
|
|
150
|
+
{ url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
|
|
151
|
+
{ url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
|
|
152
|
+
{ url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
|
|
153
|
+
{ url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
|
|
154
|
+
{ url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
|
|
155
|
+
{ url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
|
|
156
|
+
{ url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
|
|
157
|
+
{ url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
|
|
158
|
+
{ url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
|
|
159
|
+
{ url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
|
|
160
|
+
{ url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
|
|
161
|
+
{ url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
|
|
162
|
+
{ url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
|
|
163
|
+
{ url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
|
|
164
|
+
{ url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
|
|
165
|
+
{ url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
|
|
166
|
+
{ url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
|
|
167
|
+
{ url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
|
|
168
|
+
{ url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
|
|
169
|
+
{ url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
|
|
170
|
+
{ url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
|
|
171
|
+
{ url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
|
|
172
|
+
{ url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
|
|
173
|
+
{ url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
|
|
174
|
+
{ url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
|
|
175
|
+
{ url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
|
|
176
|
+
{ url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
|
|
177
|
+
{ url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
|
|
178
|
+
{ url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
|
|
179
|
+
{ url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
|
|
180
|
+
{ url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
|
|
181
|
+
{ url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
|
|
182
|
+
{ url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
|
|
183
|
+
{ url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
|
|
184
|
+
{ url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
|
|
185
|
+
{ url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
|
|
186
|
+
{ url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
|
|
187
|
+
{ url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
|
|
188
|
+
{ url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" },
|
|
189
|
+
{ url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" },
|
|
190
|
+
{ url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" },
|
|
191
|
+
{ url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" },
|
|
192
|
+
{ url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" },
|
|
193
|
+
{ url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" },
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
[[package]]
|
|
197
|
+
name = "iniconfig"
|
|
198
|
+
version = "2.3.0"
|
|
199
|
+
source = { registry = "https://pypi.org/simple" }
|
|
200
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
201
|
+
wheels = [
|
|
202
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
[[package]]
|
|
206
|
+
name = "jmespath"
|
|
207
|
+
version = "1.1.0"
|
|
208
|
+
source = { registry = "https://pypi.org/simple" }
|
|
209
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
|
|
210
|
+
wheels = [
|
|
211
|
+
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
[[package]]
|
|
215
|
+
name = "okrlinkhub-sync-indicators"
|
|
216
|
+
version = "0.3.0"
|
|
217
|
+
source = { editable = "." }
|
|
218
|
+
dependencies = [
|
|
219
|
+
{ name = "cryptography" },
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
[package.optional-dependencies]
|
|
223
|
+
r2 = [
|
|
224
|
+
{ name = "boto3" },
|
|
225
|
+
]
|
|
226
|
+
test = [
|
|
227
|
+
{ name = "pytest" },
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
[package.metadata]
|
|
231
|
+
requires-dist = [
|
|
232
|
+
{ name = "boto3", marker = "extra == 'r2'", specifier = ">=1.34" },
|
|
233
|
+
{ name = "cryptography", specifier = ">=42" },
|
|
234
|
+
{ name = "pytest", marker = "extra == 'test'", specifier = ">=8" },
|
|
235
|
+
]
|
|
236
|
+
provides-extras = ["r2", "test"]
|
|
237
|
+
|
|
238
|
+
[[package]]
|
|
239
|
+
name = "packaging"
|
|
240
|
+
version = "26.3"
|
|
241
|
+
source = { registry = "https://pypi.org/simple" }
|
|
242
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
|
243
|
+
wheels = [
|
|
244
|
+
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
[[package]]
|
|
248
|
+
name = "pluggy"
|
|
249
|
+
version = "1.6.0"
|
|
250
|
+
source = { registry = "https://pypi.org/simple" }
|
|
251
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
252
|
+
wheels = [
|
|
253
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
[[package]]
|
|
257
|
+
name = "pycparser"
|
|
258
|
+
version = "3.0"
|
|
259
|
+
source = { registry = "https://pypi.org/simple" }
|
|
260
|
+
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
|
261
|
+
wheels = [
|
|
262
|
+
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
[[package]]
|
|
266
|
+
name = "pygments"
|
|
267
|
+
version = "2.20.0"
|
|
268
|
+
source = { registry = "https://pypi.org/simple" }
|
|
269
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
270
|
+
wheels = [
|
|
271
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
[[package]]
|
|
275
|
+
name = "pytest"
|
|
276
|
+
version = "9.1.1"
|
|
277
|
+
source = { registry = "https://pypi.org/simple" }
|
|
278
|
+
dependencies = [
|
|
279
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
280
|
+
{ name = "iniconfig" },
|
|
281
|
+
{ name = "packaging" },
|
|
282
|
+
{ name = "pluggy" },
|
|
283
|
+
{ name = "pygments" },
|
|
284
|
+
]
|
|
285
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
|
286
|
+
wheels = [
|
|
287
|
+
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
[[package]]
|
|
291
|
+
name = "python-dateutil"
|
|
292
|
+
version = "2.9.0.post0"
|
|
293
|
+
source = { registry = "https://pypi.org/simple" }
|
|
294
|
+
dependencies = [
|
|
295
|
+
{ name = "six" },
|
|
296
|
+
]
|
|
297
|
+
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
|
298
|
+
wheels = [
|
|
299
|
+
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
[[package]]
|
|
303
|
+
name = "s3transfer"
|
|
304
|
+
version = "0.19.2"
|
|
305
|
+
source = { registry = "https://pypi.org/simple" }
|
|
306
|
+
dependencies = [
|
|
307
|
+
{ name = "botocore" },
|
|
308
|
+
]
|
|
309
|
+
sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" }
|
|
310
|
+
wheels = [
|
|
311
|
+
{ url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" },
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
[[package]]
|
|
315
|
+
name = "six"
|
|
316
|
+
version = "1.17.0"
|
|
317
|
+
source = { registry = "https://pypi.org/simple" }
|
|
318
|
+
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
|
319
|
+
wheels = [
|
|
320
|
+
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
[[package]]
|
|
324
|
+
name = "urllib3"
|
|
325
|
+
version = "2.7.0"
|
|
326
|
+
source = { registry = "https://pypi.org/simple" }
|
|
327
|
+
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
|
328
|
+
wheels = [
|
|
329
|
+
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
|
330
|
+
]
|