tinyfan 0.2.9__tar.gz → 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.
- {tinyfan-0.2.9 → tinyfan-0.3.0}/PKG-INFO +3 -5
- {tinyfan-0.2.9 → tinyfan-0.3.0}/pyproject.toml +3 -5
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/argo_typing.py +11 -1
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/codegen.py +78 -39
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/flow.py +11 -2
- tinyfan-0.3.0/src/tinyfan/stores/gcs_pickle.py +149 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/utils/embed.py +2 -2
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/test_codegen.py +33 -0
- tinyfan-0.3.0/tests/test_gcs_pickle_store.py +71 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/uv.lock +85 -22
- tinyfan-0.2.9/src/tinyfan/stores/gcs_pickle.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/.github/workflows/publish.yml +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/.gitignore +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/.python-version +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/README.md +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/README.md +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/configmap.yaml +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/configs.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/getting-started.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/packaged/.python-version +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/packaged/Dockerfile +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/packaged/README.md +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/packaged/pyproject.toml +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/packaged/src/tinyfan_examples_packaged/__init__.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/packaged/src/tinyfan_examples_packaged/asset.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/resource.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/examples/secret.yaml +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/__init__.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/cli.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/config.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/dagviz.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/flowrundata.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/py.typed +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/resources/__init__.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/resources/base.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/stores/__init__.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/stores/base.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/stores/bigquery.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/stores/naive.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/utils/__init__.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/utils/exjson.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/utils/merge.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/utils/typing.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/src/tinyfan/utils/yaml.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/__init__.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/codegen_samples/directory/__init__.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/codegen_samples/directory/asset.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/codegen_samples/directory/asset2.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/codegen_samples/singlefile.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/conftest.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/helpers.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/openapi2jsonschema.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/test_config.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/test_decorators.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/utils/__init__.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/utils/embed_samples/directory/__init__.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/utils/embed_samples/directory/dep.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/utils/embed_samples/singlefile.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/utils/test_embed.py +0 -0
- {tinyfan-0.2.9 → tinyfan-0.3.0}/tests/utils/test_exjson.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinyfan
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Tiny Argo Workflows python devkit
|
|
5
5
|
Project-URL: Documentation, https://github.com/eunchuldev/tinyfan#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/eunchuldev/tinyfan/issues
|
|
@@ -9,14 +9,12 @@ Author-email: eunchuldev <eunchulsong@gmail.com>
|
|
|
9
9
|
Keywords: argo-workflow,pipeline,tinyfan,workflow
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
13
|
Classifier: Typing :: Typed
|
|
16
14
|
Requires-Python: >=3.12
|
|
17
|
-
Requires-Dist: croniter>=6
|
|
15
|
+
Requires-Dist: croniter>=6
|
|
18
16
|
Requires-Dist: pyyaml>=6.0.3
|
|
19
|
-
Requires-Dist: typer-slim>=0.
|
|
17
|
+
Requires-Dist: typer-slim>=0.24.0
|
|
20
18
|
Provides-Extra: all
|
|
21
19
|
Requires-Dist: google-cloud-bigquery[pandas]; extra == 'all'
|
|
22
20
|
Requires-Dist: google-cloud-storage; extra == 'all'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tinyfan"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = 'Tiny Argo Workflows python devkit'
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -9,12 +9,10 @@ authors = [{ name = "eunchuldev", email = "eunchulsong@gmail.com" }]
|
|
|
9
9
|
classifiers = [
|
|
10
10
|
"Operating System :: OS Independent",
|
|
11
11
|
"License :: OSI Approved :: MIT License",
|
|
12
|
-
"Programming Language :: Python :: 3.10",
|
|
13
|
-
"Programming Language :: Python :: 3.11",
|
|
14
12
|
"Programming Language :: Python :: 3.12",
|
|
15
13
|
"Typing :: Typed",
|
|
16
14
|
]
|
|
17
|
-
dependencies = ["croniter>=6
|
|
15
|
+
dependencies = ["croniter>=6", "pyyaml>=6.0.3", "typer-slim>=0.24.0"]
|
|
18
16
|
|
|
19
17
|
[project.scripts]
|
|
20
18
|
tinyfan = "tinyfan.cli:main"
|
|
@@ -54,7 +52,7 @@ addopts = ["--import-mode=importlib"]
|
|
|
54
52
|
exclude = ['build']
|
|
55
53
|
|
|
56
54
|
[[tool.mypy.overrides]]
|
|
57
|
-
module = ["lark.*", "jsonpatch.*"]
|
|
55
|
+
module = ["lark.*", "jsonpatch.*", "google.*"]
|
|
58
56
|
ignore_missing_imports = true
|
|
59
57
|
|
|
60
58
|
[tool.ruff]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import TypedDict, NotRequired
|
|
1
|
+
from typing import Any, TypedDict, NotRequired
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class ExecAction(TypedDict):
|
|
@@ -194,6 +194,16 @@ class VolumeMount(TypedDict, total=False):
|
|
|
194
194
|
subPathExpr: str
|
|
195
195
|
|
|
196
196
|
|
|
197
|
+
class TemplateInputs(TypedDict, total=False):
|
|
198
|
+
artifacts: list[dict[str, Any]]
|
|
199
|
+
parameters: list[dict[str, Any]]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TemplateOutputs(TypedDict, total=False):
|
|
203
|
+
artifacts: list[dict[str, Any]]
|
|
204
|
+
parameters: list[dict[str, Any]]
|
|
205
|
+
|
|
206
|
+
|
|
197
207
|
# Ref: https://argo-workflows.readthedocs.io/en/latest/fields/#scripttemplate
|
|
198
208
|
class ScriptTemplate(TypedDict, total=False):
|
|
199
209
|
args: list[str]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from tinyfan.flow import Flow, Asset, FLOW_REGISTER, DEFAULT_IMAGE
|
|
2
|
+
import copy
|
|
2
3
|
import importlib.util
|
|
3
4
|
from typing import Self
|
|
4
5
|
from .utils.yaml import dump as yaml_dump, dump_all as yaml_dump_all
|
|
@@ -11,7 +12,7 @@ import os
|
|
|
11
12
|
import pkgutil
|
|
12
13
|
import datetime
|
|
13
14
|
import croniter
|
|
14
|
-
from .argo_typing import ScriptTemplate
|
|
15
|
+
from .argo_typing import ScriptTemplate, TemplateInputs, TemplateOutputs
|
|
15
16
|
from .utils.embed import embed
|
|
16
17
|
from .utils.merge import deepmerge, dropnone
|
|
17
18
|
from .config import CLI_CONFIG_STATE, Config, ConfigValue, ConfigMapKeyRef, ConfigMapRef, SecretKeyRef, SecretRef
|
|
@@ -23,6 +24,34 @@ VALID_DEPENDS_REGEXP = r"^(?!.*\.[A-Za-z0-9_]+\.[A-Za-z0-9_]+)(?!.*\.(?!Succeede
|
|
|
23
24
|
EXTRACT_DEPENDS_REGEXP = r"(?<!\.)\b([A-Za-z0-9_]+)\b"
|
|
24
25
|
|
|
25
26
|
RUNDATA_FILE_PATH = "/tmp/tinyfan/rundata.json"
|
|
27
|
+
INTERNAL_INPUT_PARAMETERS = {"rundata", "asset_name", "moduledata"}
|
|
28
|
+
INTERNAL_OUTPUT_PARAMETERS = {"rundata"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def merge_template_io(template_io: dict, user_io: TemplateInputs | TemplateOutputs | None, reserved_parameter_names: set[str]) -> dict:
|
|
32
|
+
result = copy.deepcopy(template_io)
|
|
33
|
+
if user_io is None:
|
|
34
|
+
return result
|
|
35
|
+
for key, value in user_io.items():
|
|
36
|
+
if key in ("parameters", "artifacts"):
|
|
37
|
+
if not isinstance(value, list):
|
|
38
|
+
raise TypeError(f"asset {key} must be a list")
|
|
39
|
+
if key == "parameters":
|
|
40
|
+
conflicts = [
|
|
41
|
+
param.get("name")
|
|
42
|
+
for param in value
|
|
43
|
+
if isinstance(param, dict) and param.get("name") in reserved_parameter_names
|
|
44
|
+
]
|
|
45
|
+
if conflicts:
|
|
46
|
+
names = ", ".join(f"`{name}`" for name in conflicts)
|
|
47
|
+
raise ValueError(f"asset parameters cannot redefine tinyfan internal parameter(s): {names}")
|
|
48
|
+
result.setdefault(key, [])
|
|
49
|
+
result[key].extend(copy.deepcopy(value))
|
|
50
|
+
elif isinstance(result.get(key), dict) and isinstance(value, dict):
|
|
51
|
+
result[key] = deepmerge(result[key], copy.deepcopy(value))
|
|
52
|
+
else:
|
|
53
|
+
result[key] = copy.deepcopy(value)
|
|
54
|
+
return result
|
|
26
55
|
|
|
27
56
|
|
|
28
57
|
def brackets_balanced(code: str) -> bool:
|
|
@@ -364,32 +393,40 @@ class AssetTree:
|
|
|
364
393
|
"synchronization": {
|
|
365
394
|
"mutexes": [{"name": f"{self.flow.name}-{node.asset.name.replace('_', '-')}"}]
|
|
366
395
|
},
|
|
367
|
-
"inputs":
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
{
|
|
374
|
-
"name": "asset_name",
|
|
375
|
-
"value": "{{= nil }}",
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
"name": "moduledata",
|
|
379
|
-
"value": "{{= nil }}",
|
|
380
|
-
},
|
|
381
|
-
]
|
|
382
|
-
},
|
|
383
|
-
"outputs": {
|
|
384
|
-
"parameters": [
|
|
385
|
-
{
|
|
386
|
-
"name": "rundata",
|
|
387
|
-
"valueFrom": {
|
|
388
|
-
"path": RUNDATA_FILE_PATH,
|
|
396
|
+
"inputs": merge_template_io(
|
|
397
|
+
{
|
|
398
|
+
"parameters": [
|
|
399
|
+
{
|
|
400
|
+
"name": "rundata",
|
|
401
|
+
"value": "{{= nil }}",
|
|
389
402
|
},
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
403
|
+
{
|
|
404
|
+
"name": "asset_name",
|
|
405
|
+
"value": "{{= nil }}",
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
"name": "moduledata",
|
|
409
|
+
"value": "{{= nil }}",
|
|
410
|
+
},
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
node.asset.inputs,
|
|
414
|
+
INTERNAL_INPUT_PARAMETERS,
|
|
415
|
+
),
|
|
416
|
+
"outputs": merge_template_io(
|
|
417
|
+
{
|
|
418
|
+
"parameters": [
|
|
419
|
+
{
|
|
420
|
+
"name": "rundata",
|
|
421
|
+
"valueFrom": {
|
|
422
|
+
"path": RUNDATA_FILE_PATH,
|
|
423
|
+
},
|
|
424
|
+
}
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
node.asset.outputs,
|
|
428
|
+
INTERNAL_OUTPUT_PARAMETERS,
|
|
429
|
+
),
|
|
393
430
|
}
|
|
394
431
|
for node in relatives
|
|
395
432
|
]
|
|
@@ -461,17 +498,19 @@ def codegen(
|
|
|
461
498
|
serviceAccountName: str | None = None,
|
|
462
499
|
) -> str:
|
|
463
500
|
"""Generate argocd workflow resource as yaml from tinyfan definitions"""
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
501
|
+
try:
|
|
502
|
+
CLI_CONFIG_STATE.codegen = True
|
|
503
|
+
if flow:
|
|
504
|
+
result = AssetTree(flow).compile(embedded, container, serviceAccountName)
|
|
505
|
+
elif location:
|
|
506
|
+
import_all_submodules(location)
|
|
507
|
+
result = "\n---\n".join(
|
|
508
|
+
AssetTree(flow).compile(embedded, container, serviceAccountName) for flow in FLOW_REGISTER.values()
|
|
509
|
+
)
|
|
510
|
+
else:
|
|
511
|
+
result = "\n---\n".join(
|
|
512
|
+
AssetTree(flow).compile(embedded, container, serviceAccountName) for flow in FLOW_REGISTER.values()
|
|
513
|
+
)
|
|
514
|
+
finally:
|
|
515
|
+
CLI_CONFIG_STATE.codegen = False
|
|
477
516
|
return result
|
|
@@ -6,7 +6,7 @@ from .flowrundata import FlowRunData, StoreIdx, UMeta
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from typing import Callable, Any, Generic, TypeVar, Awaitable, Coroutine, cast
|
|
8
8
|
from .resources.base import ResourceBase
|
|
9
|
-
from .argo_typing import ScriptTemplate
|
|
9
|
+
from .argo_typing import ScriptTemplate, TemplateInputs, TemplateOutputs
|
|
10
10
|
from .config import Config, ConfigValue
|
|
11
11
|
|
|
12
12
|
FLOW_REGISTER = {}
|
|
@@ -51,6 +51,8 @@ class Asset(Generic[Ret, UMeta, StoreIdx]):
|
|
|
51
51
|
name: str = field(init=False)
|
|
52
52
|
container: ScriptTemplate | None = None
|
|
53
53
|
serviceAccountName: str | None = None
|
|
54
|
+
inputs: TemplateInputs | None = None
|
|
55
|
+
outputs: TemplateOutputs | None = None
|
|
54
56
|
|
|
55
57
|
def __post_init__(self):
|
|
56
58
|
self.name = self.func.__name__
|
|
@@ -81,7 +83,10 @@ class Asset(Generic[Ret, UMeta, StoreIdx]):
|
|
|
81
83
|
for name, prundata in parent_flowrundatas.items():
|
|
82
84
|
parent = self.flow.assets[name]
|
|
83
85
|
index = prundata.get("store_entry_idx", None)
|
|
84
|
-
|
|
86
|
+
if index is not None:
|
|
87
|
+
data = parent.store.retrieve(index, source_rundata=prundata, target_rundata=rundata)
|
|
88
|
+
else:
|
|
89
|
+
data = None
|
|
85
90
|
if name in func_param_names:
|
|
86
91
|
params[name] = data
|
|
87
92
|
ret = self.func(**params)
|
|
@@ -118,6 +123,8 @@ def asset(
|
|
|
118
123
|
metadata: UMeta | None = None,
|
|
119
124
|
container: ScriptTemplate | None = None,
|
|
120
125
|
serviceAccountName: str | None = None,
|
|
126
|
+
inputs: TemplateInputs | None = None,
|
|
127
|
+
outputs: TemplateOutputs | None = None,
|
|
121
128
|
) -> Callable[..., AssetFunc[Ret, UMeta, StoreIdx]]:
|
|
122
129
|
def wrapper(func: Callable[..., Ret]) -> AssetFunc[Ret, UMeta, StoreIdx]:
|
|
123
130
|
asset = Asset[Ret, UMeta, StoreIdx](
|
|
@@ -130,6 +137,8 @@ def asset(
|
|
|
130
137
|
metadata=metadata,
|
|
131
138
|
container=container,
|
|
132
139
|
serviceAccountName=serviceAccountName,
|
|
140
|
+
inputs=inputs,
|
|
141
|
+
outputs=outputs,
|
|
133
142
|
)
|
|
134
143
|
return AssetFunc(func, asset)
|
|
135
144
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pickle
|
|
4
|
+
import re
|
|
5
|
+
import uuid
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any, NotRequired, TypedDict
|
|
8
|
+
|
|
9
|
+
from ..config import ConfigValue
|
|
10
|
+
from .base import FlowRunData, StoreBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GcsPickleStoreIndex(TypedDict):
|
|
14
|
+
bucket: str
|
|
15
|
+
name: str
|
|
16
|
+
generation: NotRequired[int]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GcsPickleStoreMetadata(TypedDict, total=False):
|
|
20
|
+
blob_name: str
|
|
21
|
+
prefix: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ConfigurableString = str | ConfigValue
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_GCS_EXTRA_ERROR = "GCS extra not installed. Please install with `pip install tinyfan[gcs]`."
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _new_storage_client(client_kwargs: Mapping[str, Any]) -> Any:
|
|
31
|
+
try:
|
|
32
|
+
from google.cloud import storage
|
|
33
|
+
except ImportError as exc:
|
|
34
|
+
raise ImportError(_GCS_EXTRA_ERROR) from exc
|
|
35
|
+
|
|
36
|
+
return storage.Client(**client_kwargs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve(value: ConfigurableString) -> str:
|
|
40
|
+
if isinstance(value, ConfigValue):
|
|
41
|
+
return value.get_value()
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _path_part(value: object, fallback: str) -> str:
|
|
46
|
+
text = str(value if value is not None else fallback).strip()
|
|
47
|
+
text = re.sub(r"[^A-Za-z0-9._=-]+", "-", text)
|
|
48
|
+
return text.strip(".-") or fallback
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _join_blob_name(*parts: str) -> str:
|
|
52
|
+
return "/".join(part.strip("/") for part in parts if part and part.strip("/"))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _run_part(rundata: FlowRunData[Any, Any]) -> str:
|
|
56
|
+
for key in ("data_interval_start", "ts", "ds"):
|
|
57
|
+
value = rundata.get(key)
|
|
58
|
+
if value is None:
|
|
59
|
+
continue
|
|
60
|
+
if hasattr(value, "isoformat"):
|
|
61
|
+
value = value.isoformat()
|
|
62
|
+
return _path_part(value, "manual")
|
|
63
|
+
return "manual"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GcsPickleStore(StoreBase[object, GcsPickleStoreMetadata, GcsPickleStoreIndex]):
|
|
67
|
+
bucket: ConfigurableString
|
|
68
|
+
prefix: ConfigurableString
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
bucket: ConfigurableString,
|
|
73
|
+
prefix: ConfigurableString = "tinyfan",
|
|
74
|
+
*,
|
|
75
|
+
client: Any | None = None,
|
|
76
|
+
client_kwargs: Mapping[str, Any] | None = None,
|
|
77
|
+
pickle_protocol: int = pickle.HIGHEST_PROTOCOL,
|
|
78
|
+
) -> None:
|
|
79
|
+
self.bucket = bucket
|
|
80
|
+
self.prefix = prefix
|
|
81
|
+
self._client = client
|
|
82
|
+
self._client_kwargs = dict(client_kwargs or {})
|
|
83
|
+
self.pickle_protocol = pickle_protocol
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def id() -> str:
|
|
87
|
+
return "tinyfan.gcs_pickle"
|
|
88
|
+
|
|
89
|
+
def _get_client(self) -> Any:
|
|
90
|
+
if self._client is None:
|
|
91
|
+
self._client = _new_storage_client(self._client_kwargs)
|
|
92
|
+
return self._client
|
|
93
|
+
|
|
94
|
+
def _bucket_name(self) -> str:
|
|
95
|
+
bucket = _resolve(self.bucket).strip()
|
|
96
|
+
if not bucket:
|
|
97
|
+
raise ValueError("GCS bucket name must not be empty.")
|
|
98
|
+
return bucket
|
|
99
|
+
|
|
100
|
+
def _blob_name(self, rundata: FlowRunData[GcsPickleStoreMetadata, GcsPickleStoreIndex]) -> str:
|
|
101
|
+
metadata = rundata.get("metadata")
|
|
102
|
+
if isinstance(metadata, Mapping):
|
|
103
|
+
blob_name = metadata.get("blob_name")
|
|
104
|
+
if blob_name is not None:
|
|
105
|
+
blob_name = str(blob_name).strip("/")
|
|
106
|
+
if not blob_name:
|
|
107
|
+
raise ValueError("GCS blob_name metadata must not be empty.")
|
|
108
|
+
return blob_name
|
|
109
|
+
|
|
110
|
+
prefix = str(metadata.get("prefix") or _resolve(self.prefix))
|
|
111
|
+
else:
|
|
112
|
+
prefix = _resolve(self.prefix)
|
|
113
|
+
|
|
114
|
+
flow_name = _path_part(rundata.get("flow_name"), "flow")
|
|
115
|
+
asset_name = _path_part(rundata.get("asset_name"), "asset")
|
|
116
|
+
run_id = _run_part(rundata)
|
|
117
|
+
filename = f"{uuid.uuid4().hex}.pickle"
|
|
118
|
+
return _join_blob_name(prefix, flow_name, asset_name, run_id, filename)
|
|
119
|
+
|
|
120
|
+
def store(
|
|
121
|
+
self,
|
|
122
|
+
value: object,
|
|
123
|
+
rundata: FlowRunData[GcsPickleStoreMetadata, GcsPickleStoreIndex],
|
|
124
|
+
) -> GcsPickleStoreIndex:
|
|
125
|
+
bucket_name = self._bucket_name()
|
|
126
|
+
blob = self._get_client().bucket(bucket_name).blob(self._blob_name(rundata))
|
|
127
|
+
data = pickle.dumps(value, protocol=self.pickle_protocol)
|
|
128
|
+
blob.upload_from_string(data, content_type="application/octet-stream")
|
|
129
|
+
|
|
130
|
+
index: GcsPickleStoreIndex = {
|
|
131
|
+
"bucket": bucket_name,
|
|
132
|
+
"name": blob.name,
|
|
133
|
+
}
|
|
134
|
+
generation = getattr(blob, "generation", None)
|
|
135
|
+
if generation is not None:
|
|
136
|
+
index["generation"] = int(generation)
|
|
137
|
+
return index
|
|
138
|
+
|
|
139
|
+
def retrieve(
|
|
140
|
+
self,
|
|
141
|
+
index: GcsPickleStoreIndex,
|
|
142
|
+
source_rundata: FlowRunData[GcsPickleStoreMetadata, GcsPickleStoreIndex],
|
|
143
|
+
target_rundata: FlowRunData,
|
|
144
|
+
) -> object:
|
|
145
|
+
del source_rundata, target_rundata
|
|
146
|
+
|
|
147
|
+
bucket = self._get_client().bucket(index["bucket"])
|
|
148
|
+
blob = bucket.blob(index["name"], generation=index.get("generation"))
|
|
149
|
+
return pickle.loads(blob.download_as_bytes())
|
|
@@ -54,7 +54,7 @@ def zip_directory(
|
|
|
54
54
|
files -= set().union(*map(path.glob, excludes))
|
|
55
55
|
for file_path in map(str, files):
|
|
56
56
|
arcname = os.path.relpath(file_path, start=parent_dir)
|
|
57
|
-
with open(file_path, "r") as f:
|
|
57
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
58
58
|
try:
|
|
59
59
|
source = f.read()
|
|
60
60
|
except Exception as e:
|
|
@@ -67,7 +67,7 @@ def zip_directory(
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def embed_singlefile(path: str) -> str:
|
|
70
|
-
with open(path, "r") as f:
|
|
70
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
71
71
|
sourcecode = f.read()
|
|
72
72
|
return sourcecode
|
|
73
73
|
|
|
@@ -5,6 +5,7 @@ from jsonschema.exceptions import ValidationError
|
|
|
5
5
|
from typing import Generator
|
|
6
6
|
import os
|
|
7
7
|
import pytest
|
|
8
|
+
import yaml
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
# type: ignore
|
|
@@ -103,6 +104,38 @@ def test_codegen_flow(validate_crds, sample_flow):
|
|
|
103
104
|
assert_code_is_running(actual)
|
|
104
105
|
|
|
105
106
|
|
|
107
|
+
def test_codegen_asset_inputs_outputs(validate_crds):
|
|
108
|
+
flow: Flow = Flow("test-io")
|
|
109
|
+
|
|
110
|
+
@asset(
|
|
111
|
+
flow=flow,
|
|
112
|
+
schedule="@daily",
|
|
113
|
+
inputs={"parameters": [{"name": "message", "value": "hello"}]},
|
|
114
|
+
outputs={"parameters": [{"name": "summary", "valueFrom": {"path": "/tmp/summary.txt"}}]},
|
|
115
|
+
)
|
|
116
|
+
def asset1(): # type: ignore
|
|
117
|
+
return "hello"
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
actual = codegen(flow=flow)
|
|
121
|
+
validate_crds(actual)
|
|
122
|
+
manifest = next(yaml.load_all(actual, yaml.Loader))
|
|
123
|
+
template = next(
|
|
124
|
+
template
|
|
125
|
+
for template in manifest["spec"]["workflowSpec"]["templates"]
|
|
126
|
+
if template["name"] == "asset1"
|
|
127
|
+
)
|
|
128
|
+
input_params = {param["name"]: param for param in template["inputs"]["parameters"]}
|
|
129
|
+
output_params = {param["name"]: param for param in template["outputs"]["parameters"]}
|
|
130
|
+
|
|
131
|
+
assert input_params["message"] == {"name": "message", "value": "hello"}
|
|
132
|
+
assert {"rundata", "asset_name", "moduledata"}.issubset(input_params.keys())
|
|
133
|
+
assert output_params["summary"] == {"name": "summary", "valueFrom": {"path": "/tmp/summary.txt"}}
|
|
134
|
+
assert output_params["rundata"] == {"name": "rundata", "valueFrom": {"path": "/tmp/tinyfan/rundata.json"}}
|
|
135
|
+
finally:
|
|
136
|
+
FLOW_REGISTER.clear()
|
|
137
|
+
|
|
138
|
+
|
|
106
139
|
def test_codegen_implicit(validate_crds, sample_flow):
|
|
107
140
|
actual = codegen()
|
|
108
141
|
validate_crds(actual)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from tinyfan.stores.gcs_pickle import GcsPickleStore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FakeBlob:
|
|
9
|
+
def __init__(self, name: str, generation: int | None = None) -> None:
|
|
10
|
+
self.name = name
|
|
11
|
+
self.generation = generation
|
|
12
|
+
self.content_type: str | None = None
|
|
13
|
+
self.data: bytes | None = None
|
|
14
|
+
|
|
15
|
+
def upload_from_string(self, data: bytes, content_type: str) -> None:
|
|
16
|
+
self.data = data
|
|
17
|
+
self.content_type = content_type
|
|
18
|
+
self.generation = (self.generation or 0) + 1
|
|
19
|
+
|
|
20
|
+
def download_as_bytes(self) -> bytes:
|
|
21
|
+
assert self.data is not None
|
|
22
|
+
return self.data
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FakeBucket:
|
|
26
|
+
def __init__(self, name: str) -> None:
|
|
27
|
+
self.name = name
|
|
28
|
+
self.blobs: dict[str, FakeBlob] = {}
|
|
29
|
+
|
|
30
|
+
def blob(self, name: str, generation: int | None = None) -> FakeBlob:
|
|
31
|
+
blob = self.blobs.setdefault(name, FakeBlob(name, generation))
|
|
32
|
+
blob.generation = generation or blob.generation
|
|
33
|
+
return blob
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FakeClient:
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
self.buckets: dict[str, FakeBucket] = {}
|
|
39
|
+
|
|
40
|
+
def bucket(self, name: str) -> FakeBucket:
|
|
41
|
+
return self.buckets.setdefault(name, FakeBucket(name))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_gcs_pickle_store_roundtrip() -> None:
|
|
45
|
+
client = FakeClient()
|
|
46
|
+
store = GcsPickleStore("test-bucket", prefix="runs", client=client)
|
|
47
|
+
value = {"hello": ["world", 1]}
|
|
48
|
+
rundata = {
|
|
49
|
+
"flow_name": "flow",
|
|
50
|
+
"asset_name": "asset",
|
|
51
|
+
"ds": "2026-05-31",
|
|
52
|
+
"data_interval_start": datetime(2026, 5, 31, 12, 30, tzinfo=UTC),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
index = store.store(value, rundata)
|
|
56
|
+
|
|
57
|
+
assert index["bucket"] == "test-bucket"
|
|
58
|
+
assert index["name"].startswith("runs/flow/asset/2026-05-31T12-30-00-00-00/")
|
|
59
|
+
assert index["name"].endswith(".pickle")
|
|
60
|
+
assert index["generation"] == 1
|
|
61
|
+
assert client.bucket("test-bucket").blob(index["name"]).content_type == "application/octet-stream"
|
|
62
|
+
assert store.retrieve(index, source_rundata=rundata, target_rundata={}) == value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_gcs_pickle_store_uses_metadata_blob_name() -> None:
|
|
66
|
+
store = GcsPickleStore("test-bucket", client=FakeClient())
|
|
67
|
+
rundata = {"metadata": {"blob_name": "/custom/path/value.pkl"}}
|
|
68
|
+
|
|
69
|
+
index = store.store("value", rundata)
|
|
70
|
+
|
|
71
|
+
assert index["name"] == "custom/path/value.pkl"
|
|
@@ -2,6 +2,15 @@ version = 1
|
|
|
2
2
|
revision = 3
|
|
3
3
|
requires-python = ">=3.12"
|
|
4
4
|
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "annotated-doc"
|
|
7
|
+
version = "0.0.4"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
5
14
|
[[package]]
|
|
6
15
|
name = "arrow"
|
|
7
16
|
version = "1.3.0"
|
|
@@ -81,18 +90,6 @@ wheels = [
|
|
|
81
90
|
{ url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446, upload-time = "2024-10-09T07:40:19.383Z" },
|
|
82
91
|
]
|
|
83
92
|
|
|
84
|
-
[[package]]
|
|
85
|
-
name = "click"
|
|
86
|
-
version = "8.1.7"
|
|
87
|
-
source = { registry = "https://pypi.org/simple" }
|
|
88
|
-
dependencies = [
|
|
89
|
-
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
90
|
-
]
|
|
91
|
-
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" }
|
|
92
|
-
wheels = [
|
|
93
|
-
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" },
|
|
94
|
-
]
|
|
95
|
-
|
|
96
93
|
[[package]]
|
|
97
94
|
name = "colorama"
|
|
98
95
|
version = "0.4.6"
|
|
@@ -397,6 +394,27 @@ wheels = [
|
|
|
397
394
|
{ url = "https://files.pythonhosted.org/packages/ee/07/44bd408781594c4d0a027666ef27fab1e441b109dc3b76b4f836f8fd04fe/jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c", size = 18482, upload-time = "2023-12-25T15:16:51.997Z" },
|
|
398
395
|
]
|
|
399
396
|
|
|
397
|
+
[[package]]
|
|
398
|
+
name = "markdown-it-py"
|
|
399
|
+
version = "4.2.0"
|
|
400
|
+
source = { registry = "https://pypi.org/simple" }
|
|
401
|
+
dependencies = [
|
|
402
|
+
{ name = "mdurl" },
|
|
403
|
+
]
|
|
404
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
|
405
|
+
wheels = [
|
|
406
|
+
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
[[package]]
|
|
410
|
+
name = "mdurl"
|
|
411
|
+
version = "0.1.2"
|
|
412
|
+
source = { registry = "https://pypi.org/simple" }
|
|
413
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
414
|
+
wheels = [
|
|
415
|
+
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
416
|
+
]
|
|
417
|
+
|
|
400
418
|
[[package]]
|
|
401
419
|
name = "mypy"
|
|
402
420
|
version = "1.14.0"
|
|
@@ -591,6 +609,15 @@ wheels = [
|
|
|
591
609
|
{ url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537, upload-time = "2024-09-11T16:02:10.336Z" },
|
|
592
610
|
]
|
|
593
611
|
|
|
612
|
+
[[package]]
|
|
613
|
+
name = "pygments"
|
|
614
|
+
version = "2.20.0"
|
|
615
|
+
source = { registry = "https://pypi.org/simple" }
|
|
616
|
+
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" }
|
|
617
|
+
wheels = [
|
|
618
|
+
{ 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" },
|
|
619
|
+
]
|
|
620
|
+
|
|
594
621
|
[[package]]
|
|
595
622
|
name = "pytest"
|
|
596
623
|
version = "8.3.3"
|
|
@@ -722,6 +749,19 @@ wheels = [
|
|
|
722
749
|
{ url = "https://files.pythonhosted.org/packages/65/d4/f7407c3d15d5ac779c3dd34fbbc6ea2090f77bd7dd12f207ccf881551208/rfc3987-1.3.8-py2.py3-none-any.whl", hash = "sha256:10702b1e51e5658843460b189b185c0366d2cf4cff716f13111b0ea9fd2dce53", size = 13377, upload-time = "2018-07-29T17:23:45.313Z" },
|
|
723
750
|
]
|
|
724
751
|
|
|
752
|
+
[[package]]
|
|
753
|
+
name = "rich"
|
|
754
|
+
version = "15.0.0"
|
|
755
|
+
source = { registry = "https://pypi.org/simple" }
|
|
756
|
+
dependencies = [
|
|
757
|
+
{ name = "markdown-it-py" },
|
|
758
|
+
{ name = "pygments" },
|
|
759
|
+
]
|
|
760
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
|
761
|
+
wheels = [
|
|
762
|
+
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
|
763
|
+
]
|
|
764
|
+
|
|
725
765
|
[[package]]
|
|
726
766
|
name = "rpds-py"
|
|
727
767
|
version = "0.20.0"
|
|
@@ -793,6 +833,15 @@ wheels = [
|
|
|
793
833
|
{ url = "https://files.pythonhosted.org/packages/3e/14/fd026bc74ded05e2351681545a5f626e78ef831f8edce064d61acd2e6ec7/ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93", size = 8679879, upload-time = "2024-10-04T13:40:25.797Z" },
|
|
794
834
|
]
|
|
795
835
|
|
|
836
|
+
[[package]]
|
|
837
|
+
name = "shellingham"
|
|
838
|
+
version = "1.5.4"
|
|
839
|
+
source = { registry = "https://pypi.org/simple" }
|
|
840
|
+
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
|
841
|
+
wheels = [
|
|
842
|
+
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
|
843
|
+
]
|
|
844
|
+
|
|
796
845
|
[[package]]
|
|
797
846
|
name = "six"
|
|
798
847
|
version = "1.16.0"
|
|
@@ -804,7 +853,7 @@ wheels = [
|
|
|
804
853
|
|
|
805
854
|
[[package]]
|
|
806
855
|
name = "tinyfan"
|
|
807
|
-
version = "0.
|
|
856
|
+
version = "0.3.0"
|
|
808
857
|
source = { editable = "." }
|
|
809
858
|
dependencies = [
|
|
810
859
|
{ name = "croniter" },
|
|
@@ -872,7 +921,7 @@ requires-dist = [
|
|
|
872
921
|
{ name = "tinyfan", extras = ["pandas"], marker = "extra == 'all'" },
|
|
873
922
|
{ name = "tinyfan", extras = ["pandas"], marker = "extra == 'bigquery'" },
|
|
874
923
|
{ name = "tinyfan", extras = ["viz"], marker = "extra == 'all'" },
|
|
875
|
-
{ name = "typer-slim", specifier = ">=0.
|
|
924
|
+
{ name = "typer-slim", specifier = ">=0.24.0" },
|
|
876
925
|
{ name = "types-croniter", marker = "extra == 'dev'" },
|
|
877
926
|
{ name = "types-jsonschema", marker = "extra == 'dev'" },
|
|
878
927
|
{ name = "types-networkx", marker = "extra == 'dev'" },
|
|
@@ -880,17 +929,31 @@ requires-dist = [
|
|
|
880
929
|
]
|
|
881
930
|
provides-extras = ["all", "gcs", "viz", "bigquery", "pandas", "dev"]
|
|
882
931
|
|
|
932
|
+
[[package]]
|
|
933
|
+
name = "typer"
|
|
934
|
+
version = "0.26.4"
|
|
935
|
+
source = { registry = "https://pypi.org/simple" }
|
|
936
|
+
dependencies = [
|
|
937
|
+
{ name = "annotated-doc" },
|
|
938
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
939
|
+
{ name = "rich" },
|
|
940
|
+
{ name = "shellingham" },
|
|
941
|
+
]
|
|
942
|
+
sdist = { url = "https://files.pythonhosted.org/packages/8e/d3/90c1ee19209cb59f6ad185883fd4ccfcf72f8f0bfd549d5a8b70474611d0/typer-0.26.4.tar.gz", hash = "sha256:25b128964de66c5ea36d5ac82adc579e5e113509b17469edf9f5a4a1864ff2a9", size = 201191, upload-time = "2026-05-30T17:05:04.213Z" }
|
|
943
|
+
wheels = [
|
|
944
|
+
{ url = "https://files.pythonhosted.org/packages/f0/6d/5a525c69df4a90892135e5d490b00e9e46402491f3416d4395fcb0d0201e/typer-0.26.4-py3-none-any.whl", hash = "sha256:11bfd7b43557137e373c2b10f6967a555f9678a61ed72c808968b011d95534d6", size = 122436, upload-time = "2026-05-30T17:05:05.812Z" },
|
|
945
|
+
]
|
|
946
|
+
|
|
883
947
|
[[package]]
|
|
884
948
|
name = "typer-slim"
|
|
885
|
-
version = "0.
|
|
949
|
+
version = "0.24.0"
|
|
886
950
|
source = { registry = "https://pypi.org/simple" }
|
|
887
951
|
dependencies = [
|
|
888
|
-
{ name = "
|
|
889
|
-
{ name = "typing-extensions" },
|
|
952
|
+
{ name = "typer" },
|
|
890
953
|
]
|
|
891
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
954
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" }
|
|
892
955
|
wheels = [
|
|
893
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
956
|
+
{ url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" },
|
|
894
957
|
]
|
|
895
958
|
|
|
896
959
|
[[package]]
|
|
@@ -916,14 +979,14 @@ wheels = [
|
|
|
916
979
|
|
|
917
980
|
[[package]]
|
|
918
981
|
name = "types-networkx"
|
|
919
|
-
version = "3.
|
|
982
|
+
version = "3.6.1.20260518"
|
|
920
983
|
source = { registry = "https://pypi.org/simple" }
|
|
921
984
|
dependencies = [
|
|
922
985
|
{ name = "numpy" },
|
|
923
986
|
]
|
|
924
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
987
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/9f/e6fe2543aecbfe4d67fe86f280afae1e6d532c49755c5fb6c1f05f8af91e/types_networkx-3.6.1.20260518.tar.gz", hash = "sha256:af3ab153815b366c3695a0015cc75aece9b3eb0e5df56bb60ffee0b7cec8f630", size = 73977, upload-time = "2026-05-18T06:06:05.573Z" }
|
|
925
988
|
wheels = [
|
|
926
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
989
|
+
{ url = "https://files.pythonhosted.org/packages/1f/4f/422a91e9fa008b804746fc2ce4da5bcb7eca4bfb45c31e8282fbb3664601/types_networkx-3.6.1.20260518-py3-none-any.whl", hash = "sha256:574304deea33828fb32cdca757af84f79b67bd80164aa1725c52a95d4a9347e6", size = 162454, upload-time = "2026-05-18T06:06:04.417Z" },
|
|
927
990
|
]
|
|
928
991
|
|
|
929
992
|
[[package]]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|