tinyfan 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {tinyfan-0.1.1 → tinyfan-0.2.0}/PKG-INFO +2 -1
  2. tinyfan-0.2.0/examples/README.md +12 -0
  3. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/configs.py +1 -1
  4. {tinyfan-0.1.1 → tinyfan-0.2.0}/pyproject.toml +9 -31
  5. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/codegen.py +24 -12
  6. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/flow.py +4 -0
  7. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/utils/merge.py +10 -0
  8. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/test_codegen.py +12 -1
  9. {tinyfan-0.1.1 → tinyfan-0.2.0}/uv.lock +14 -1
  10. {tinyfan-0.1.1 → tinyfan-0.2.0}/.github/workflows/publish.yml +0 -0
  11. {tinyfan-0.1.1 → tinyfan-0.2.0}/.gitignore +0 -0
  12. {tinyfan-0.1.1 → tinyfan-0.2.0}/.python-version +0 -0
  13. {tinyfan-0.1.1 → tinyfan-0.2.0}/README.md +0 -0
  14. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/configmap.yaml +0 -0
  15. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/getting-started.py +0 -0
  16. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/packaged/.python-version +0 -0
  17. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/packaged/Dockerfile +0 -0
  18. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/packaged/README.md +0 -0
  19. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/packaged/pyproject.toml +0 -0
  20. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/packaged/src/tinyfan_examples_packaged/__init__.py +0 -0
  21. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/packaged/src/tinyfan_examples_packaged/asset.py +0 -0
  22. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/resource.py +0 -0
  23. {tinyfan-0.1.1 → tinyfan-0.2.0}/examples/secret.yaml +0 -0
  24. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/__init__.py +0 -0
  25. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/argo_typing.py +0 -0
  26. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/cli.py +0 -0
  27. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/config.py +0 -0
  28. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/flowrundata.py +0 -0
  29. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/resources/__init__.py +0 -0
  30. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/resources/base.py +0 -0
  31. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/stores/__init__.py +0 -0
  32. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/stores/base.py +0 -0
  33. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/stores/bigquery.py +0 -0
  34. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/stores/gcs_pickle.py +0 -0
  35. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/stores/naive.py +0 -0
  36. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/utils/__init__.py +0 -0
  37. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/utils/embed.py +0 -0
  38. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/utils/exjson.py +0 -0
  39. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/utils/typing.py +0 -0
  40. {tinyfan-0.1.1 → tinyfan-0.2.0}/src/tinyfan/utils/yaml.py +0 -0
  41. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/__init__.py +0 -0
  42. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/codegen_samples/directory/__init__.py +0 -0
  43. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/codegen_samples/directory/asset.py +0 -0
  44. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/codegen_samples/directory/asset2.py +0 -0
  45. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/codegen_samples/singlefile.py +0 -0
  46. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/conftest.py +0 -0
  47. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/helpers.py +0 -0
  48. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/openapi2jsonschema.py +0 -0
  49. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/test_decorators.py +0 -0
  50. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/utils/__init__.py +0 -0
  51. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/utils/embed_samples/directory/__init__.py +0 -0
  52. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/utils/embed_samples/directory/dep.py +0 -0
  53. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/utils/embed_samples/singlefile.py +0 -0
  54. {tinyfan-0.1.1 → tinyfan-0.2.0}/tests/utils/test_embed.py +0 -0
  55. {tinyfan-0.1.1 → tinyfan-0.2.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.1.1
3
+ Version: 0.2.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
@@ -31,6 +31,7 @@ Requires-Dist: jsonschema[format]; extra == 'dev'
31
31
  Requires-Dist: mypy>=1.14; extra == 'dev'
32
32
  Requires-Dist: pytest>=8.3.3; extra == 'dev'
33
33
  Requires-Dist: ruff; extra == 'dev'
34
+ Requires-Dist: types-croniter; extra == 'dev'
34
35
  Requires-Dist: types-jsonschema; extra == 'dev'
35
36
  Requires-Dist: types-pyyaml; extra == 'dev'
36
37
  Provides-Extra: gcs
@@ -0,0 +1,12 @@
1
+ # How to run
2
+
3
+ ```bash
4
+ # setup k3d
5
+ k3d cluster create tinyfan
6
+ kubectl config use-context k3d-tinyfan
7
+ # install argowf
8
+ kubectl create namespace argo
9
+ kubectl apply -n argo -f "https://github.com/argoproj/argo-workflows/releases/download/v3.6.4/quick-start-minimal.yaml"
10
+ # apply manifests
11
+ tinyfan examples/getting-started.py | kubectl apply -n argo -f -
12
+ ```
@@ -27,5 +27,5 @@ def trace_configs(
27
27
  secretkey: SecretKeyRef,
28
28
  ):
29
29
  print(
30
- f'{constant} - {cliarg} - {configmap.get("otherkey")} - {configmapkey.get_value()} - {secret.get("othersecretkey")} - {secretkey.get_value()}'
30
+ f"{constant} - {cliarg} - {configmap.get('otherkey')} - {configmapkey.get_value()} - {secret.get('othersecretkey')} - {secretkey.get_value()}"
31
31
  )
@@ -1,18 +1,11 @@
1
1
  [project]
2
2
  name = "tinyfan"
3
- version = "0.1.1"
3
+ version = "0.2.0"
4
4
  description = 'Tiny Argo Workflows python devkit'
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
- keywords = [
8
- "argo-workflow",
9
- "tinyfan",
10
- "workflow",
11
- "pipeline",
12
- ]
13
- authors = [
14
- { name = "eunchuldev", email = "eunchulsong@gmail.com" },
15
- ]
7
+ keywords = ["argo-workflow", "tinyfan", "workflow", "pipeline"]
8
+ authors = [{ name = "eunchuldev", email = "eunchulsong@gmail.com" }]
16
9
  classifiers = [
17
10
  "Operating System :: OS Independent",
18
11
  "License :: OSI Approved :: MIT License",
@@ -20,32 +13,16 @@ classifiers = [
20
13
  "Programming Language :: Python :: 3.11",
21
14
  "Programming Language :: Python :: 3.12",
22
15
  ]
23
- dependencies = [
24
- "croniter>=3.0.3",
25
- "pyyaml>=6.0.2",
26
- "typer-slim>=0.15.1",
27
- ]
16
+ dependencies = ["croniter>=3.0.3", "pyyaml>=6.0.2", "typer-slim>=0.15.1"]
28
17
 
29
18
  [project.scripts]
30
19
  tinyfan = "tinyfan.cli:main"
31
20
 
32
21
  [project.optional-dependencies]
33
- all = [
34
- "tinyfan[gcs]",
35
- "tinyfan[bigquery]",
36
- "tinyfan[pandas]",
37
- ]
38
- gcs = [
39
- "google-cloud-storage",
40
- ]
41
- bigquery = [
42
- "google-cloud-bigquery[pandas]",
43
- "tinyfan[pandas]",
44
- ]
45
- pandas = [
46
- "pandas",
47
- "pyarrow",
48
- ]
22
+ all = ["tinyfan[gcs]", "tinyfan[bigquery]", "tinyfan[pandas]"]
23
+ gcs = ["google-cloud-storage"]
24
+ bigquery = ["google-cloud-bigquery[pandas]", "tinyfan[pandas]"]
25
+ pandas = ["pandas", "pyarrow"]
49
26
  dev = [
50
27
  "pytest >= 8.3.3",
51
28
  "jsonpatch>=1",
@@ -54,6 +31,7 @@ dev = [
54
31
  "ruff",
55
32
  "types-PyYAML",
56
33
  "types-jsonschema",
34
+ "types-croniter",
57
35
  ]
58
36
 
59
37
  [project.urls]
@@ -13,8 +13,9 @@ import datetime
13
13
  import croniter
14
14
  from .argo_typing import ScriptTemplate
15
15
  from .utils.embed import embed
16
- from .utils.merge import deepmerge
16
+ from .utils.merge import deepmerge, dropnone
17
17
  from .config import CLI_CONFIG_STATE, Config, ConfigValue, ConfigMapKeyRef, ConfigMapRef, SecretKeyRef, SecretRef
18
+ from .flowrundata import FlowRunData
18
19
 
19
20
  VALID_CRON_REGEXP = r"^(?:(?:(?:(?:\d+,)+\d+|(?:\d+(?:\/|-|#)\d+)|\d+L?|\*(?:\/\d+)?|L(?:-\d+)?|\?|[A-Z]{3}(?:-[A-Z]{3})?) ?){5,7})|(@hourly|@daily|@midnight|@weekly|@monthly|@yearly|@annually)$"
20
21
  VALID_DEPENDS_REGEXP = r"^(?!.*\.[A-Za-z0-9_]+\.[A-Za-z0-9_]+)(?!.*\.(?!Succeeded|Failed|Errored|Skipped|Omitted|Daemoned)\w+)(?!.*(?:&&|\|\|)\s*(?:&&|\|\|))(?!.*!!)[A-Za-z0-9_&|!().\s]+$"
@@ -88,19 +89,21 @@ class AssetNode:
88
89
  t1 = cron.get_next(datetime.datetime)
89
90
  t2 = cron.get_next(datetime.datetime)
90
91
  duration_sec = (t2 - t1).total_seconds()
92
+ data_interval_start_tmpl = (
93
+ f"sprig.dateModify('-{duration_sec}s', sprig.toDate('2006-01-02T15:04:05Z07:00', workflow.scheduledTime))"
94
+ )
91
95
  return re.sub(
92
96
  r'"(\{\{tasks\.[^.]+\.outputs\.parameters\.rundata\}\})"',
93
97
  r"\1",
94
98
  json.dumps(
95
99
  {
96
- "ds": "{{=sprig.date('2006-01-02', workflow.scheduledTime)}}",
97
- "ts": "{{workflow.scheduledTime}}",
100
+ "ds": "{{=sprig.date('2006-01-02', %s)}}" % data_interval_start_tmpl,
101
+ "ts": "{{=%s}}" % data_interval_start_tmpl,
98
102
  "data_interval_start": {
99
- DATETIME_ANNOTATION: "{{workflow.scheduledTime}}",
103
+ DATETIME_ANNOTATION: "{{=%s}}" % data_interval_start_tmpl,
100
104
  },
101
105
  "data_interval_end": {
102
- DATETIME_ANNOTATION: "{{=sprig.dateModify('%ss', sprig.toDate('2006-01-02T15:04:05Z07:00', workflow.scheduledTime))}}"
103
- % duration_sec,
106
+ DATETIME_ANNOTATION: "{{workflow.scheduledTime}}",
104
107
  },
105
108
  "parents": {
106
109
  p.asset.name: "{{tasks.%s.outputs.parameters.rundata}}" % p.asset.name for p in self.parents
@@ -136,6 +139,7 @@ class AssetTree:
136
139
  set(self.flow.assets.keys())
137
140
  | set((self.flow.resources or {}).keys())
138
141
  | set((self.flow.configs or {}).keys())
142
+ | set(inspect.get_annotations(FlowRunData).keys())
139
143
  )
140
144
  unknown_names = [n for n in func_param_names if n not in known_names]
141
145
  if len(unknown_names) > 0:
@@ -169,6 +173,7 @@ class AssetTree:
169
173
  self,
170
174
  embedded: bool = True,
171
175
  container: ScriptTemplate = {},
176
+ serviceAccountName: str | None = None,
172
177
  ) -> str:
173
178
  if len(self.nodes) == 0:
174
179
  return ""
@@ -184,7 +189,7 @@ class AssetTree:
184
189
  root_location = get_root_path(list(self.nodes.values())[0].asset.func)
185
190
  source += embed(root_location)
186
191
  if not root_location.endswith(".py"):
187
- source += "from {{inputs.parameters.module_name}}" " import {{inputs.parameters.asset_name}}\n"
192
+ source += "from {{inputs.parameters.module_name}} import {{inputs.parameters.asset_name}}\n"
188
193
  source += "asset = {{inputs.parameters.asset_name}}.asset\n"
189
194
  else:
190
195
  source = (
@@ -217,14 +222,15 @@ class AssetTree:
217
222
  "apiVersion": "argoproj.io/v1alpha1",
218
223
  "kind": "CronWorkflow",
219
224
  "metadata": {
220
- "name": f"""{self.flow.name}-{ encode_schedule_to_k8sname(tz, schedule) }""",
221
- "generateName": f"""{self.flow.name}-{ encode_schedule_to_k8sname(tz, schedule) }-""",
225
+ "name": f"""{self.flow.name}-{encode_schedule_to_k8sname(tz, schedule)}""",
226
+ "generateName": f"""{self.flow.name}-{encode_schedule_to_k8sname(tz, schedule)}-""",
222
227
  },
223
228
  "spec": {
224
229
  "schedule": schedule,
225
230
  "timezone": tz,
226
231
  "workflowSpec": {
227
232
  "entrypoint": "flow",
233
+ **dropnone({"serviceAccountName": serviceAccountName or self.flow.serviceAccountName}),
228
234
  "podSpecPatch": yaml_dump(
229
235
  {
230
236
  "containers": [
@@ -247,6 +253,7 @@ class AssetTree:
247
253
  "templates": [
248
254
  {
249
255
  "name": node.asset.name.replace("_", "-"),
256
+ **dropnone({"serviceAccountName": node.asset.serviceAccountName}),
250
257
  "script": deepmerge(
251
258
  {
252
259
  "image": DEFAULT_IMAGE,
@@ -389,15 +396,20 @@ def codegen(
389
396
  location: str | None = None,
390
397
  embedded: bool = False,
391
398
  container: ScriptTemplate = {},
399
+ serviceAccountName: str | None = None,
392
400
  ) -> str:
393
401
  """Generate argocd workflow resource as yaml from tinyfan definitions"""
394
402
  CLI_CONFIG_STATE.codegen = True
395
403
  if flow:
396
- result = AssetTree(flow).compile(embedded, container)
404
+ result = AssetTree(flow).compile(embedded, container, serviceAccountName)
397
405
  elif location:
398
406
  import_all_submodules(location)
399
- result = "\n---\n".join(AssetTree(flow).compile(embedded, container) for flow in FLOW_REGISTER.values())
407
+ result = "\n---\n".join(
408
+ AssetTree(flow).compile(embedded, container, serviceAccountName) for flow in FLOW_REGISTER.values()
409
+ )
400
410
  else:
401
- result = "\n---\n".join(AssetTree(flow).compile(embedded, container) for flow in FLOW_REGISTER.values())
411
+ result = "\n---\n".join(
412
+ AssetTree(flow).compile(embedded, container, serviceAccountName) for flow in FLOW_REGISTER.values()
413
+ )
402
414
  CLI_CONFIG_STATE.codegen = False
403
415
  return result
@@ -22,6 +22,7 @@ class Flow:
22
22
  assets: dict[str, "Asset[Any, Any, Any]"] = field(default_factory=dict)
23
23
  resources: dict[str, ResourceBase] | None = None
24
24
  container: ScriptTemplate | None = None
25
+ serviceAccountName: str | None = None
25
26
  store: StoreBase = field(default_factory=NaiveStore)
26
27
  configs: dict[str, Config | ConfigValue | str | None] = field(default_factory=dict)
27
28
 
@@ -48,6 +49,7 @@ class Asset(Generic[Ret, UMeta, StoreIdx]):
48
49
  depends: str | None = None
49
50
  name: str = field(init=False)
50
51
  container: ScriptTemplate | None = None
52
+ serviceAccountName: str | None = None
51
53
 
52
54
  def __post_init__(self):
53
55
  self.name = self.func.__name__
@@ -110,6 +112,7 @@ def asset(
110
112
  tz: str | None = None,
111
113
  metadata: UMeta | None = None,
112
114
  container: ScriptTemplate | None = None,
115
+ serviceAccountName: str | None = None,
113
116
  ) -> Callable[..., AssetFunc[Ret, UMeta, StoreIdx]]:
114
117
  def wrapper(func: Callable[..., Ret]) -> AssetFunc[Ret, UMeta, StoreIdx]:
115
118
  asset = Asset[Ret, UMeta, StoreIdx](
@@ -121,6 +124,7 @@ def asset(
121
124
  tz=tz or flow.tz,
122
125
  metadata=metadata,
123
126
  container=container,
127
+ serviceAccountName=serviceAccountName,
124
128
  )
125
129
  return AssetFunc(func, asset)
126
130
 
@@ -15,3 +15,13 @@ def deepmerge(dict1: T, dict2: T, *args: T) -> T:
15
15
  for arg in args:
16
16
  deepmerge(dict1, arg)
17
17
  return dict1
18
+
19
+
20
+ def dropnone(d: dict) -> dict:
21
+ dropping = []
22
+ for k in d:
23
+ if d[k] is None:
24
+ dropping.append(k)
25
+ for k in dropping:
26
+ del d[k]
27
+ return d
@@ -13,7 +13,7 @@ def sample_flow() -> Generator:
13
13
  flow: Flow = Flow("test")
14
14
 
15
15
  @asset(flow=flow, schedule="@daily")
16
- def asset1(): # type: ignore
16
+ def asset1(ds: str): # type: ignore
17
17
  print("asset1 is executed")
18
18
  return "hello"
19
19
 
@@ -127,3 +127,14 @@ def test_codegen_directory(validate_crds):
127
127
  if e.json_path != "$.spec.templates[0].script" or e.validator != "required":
128
128
  raise e
129
129
  assert_code_is_running(actual)
130
+
131
+
132
+ def test_rundata_data_interval(sample_flow):
133
+ tree = AssetTree(sample_flow)
134
+ node = list(tree.nodes.values())[0]
135
+ # rundatatmpl = node.rundatatmpl("0 * * * * *")
136
+ daily = node.rundatatmpl("@daily")
137
+ assert (
138
+ daily
139
+ == """{"ds": "{{=sprig.date('2006-01-02', sprig.dateModify('-86400.0s', sprig.toDate('2006-01-02T15:04:05Z07:00', workflow.scheduledTime)))}}", "ts": "{{=sprig.dateModify('-86400.0s', sprig.toDate('2006-01-02T15:04:05Z07:00', workflow.scheduledTime))}}", "data_interval_start": {"__tinyfan_datetime__": "{{=sprig.dateModify('-86400.0s', sprig.toDate('2006-01-02T15:04:05Z07:00', workflow.scheduledTime))}}"}, "data_interval_end": {"__tinyfan_datetime__": "{{workflow.scheduledTime}}"}, "parents": {}, "asset_name": "asset1", "flow_name": "test", "module_name": "tests.test_codegen"}"""
140
+ )
@@ -1,4 +1,5 @@
1
1
  version = 1
2
+ revision = 1
2
3
  requires-python = ">=3.12"
3
4
 
4
5
  [[package]]
@@ -774,7 +775,7 @@ wheels = [
774
775
 
775
776
  [[package]]
776
777
  name = "tinyfan"
777
- version = "0.1.1"
778
+ version = "0.2.0"
778
779
  source = { editable = "." }
779
780
  dependencies = [
780
781
  { name = "croniter" },
@@ -800,6 +801,7 @@ dev = [
800
801
  { name = "mypy" },
801
802
  { name = "pytest" },
802
803
  { name = "ruff" },
804
+ { name = "types-croniter" },
803
805
  { name = "types-jsonschema" },
804
806
  { name = "types-pyyaml" },
805
807
  ]
@@ -829,9 +831,11 @@ requires-dist = [
829
831
  { name = "tinyfan", extras = ["pandas"], marker = "extra == 'all'" },
830
832
  { name = "tinyfan", extras = ["pandas"], marker = "extra == 'bigquery'" },
831
833
  { name = "typer-slim", specifier = ">=0.15.1" },
834
+ { name = "types-croniter", marker = "extra == 'dev'" },
832
835
  { name = "types-jsonschema", marker = "extra == 'dev'" },
833
836
  { name = "types-pyyaml", marker = "extra == 'dev'" },
834
837
  ]
838
+ provides-extras = ["all", "gcs", "bigquery", "pandas", "dev"]
835
839
 
836
840
  [[package]]
837
841
  name = "typer-slim"
@@ -846,6 +850,15 @@ wheels = [
846
850
  { url = "https://files.pythonhosted.org/packages/1f/7b/032ecd581e2170513bb6dc3cdb2581e20fdb94a272bae70fe93f2bca580b/typer_slim-0.15.1-py3-none-any.whl", hash = "sha256:20233cb89938ea3cca633afee10b906a1b0e7c5330f31ed8c55f4f0779efe6df", size = 44968 },
847
851
  ]
848
852
 
853
+ [[package]]
854
+ name = "types-croniter"
855
+ version = "5.0.1.20241205"
856
+ source = { registry = "https://pypi.org/simple" }
857
+ sdist = { url = "https://files.pythonhosted.org/packages/77/2f/f3298f148a7983df2a878d53c4a17ef1dd8654a155e11caa8decb7d79670/types_croniter-5.0.1.20241205.tar.gz", hash = "sha256:8a7cb10aa0b487c654a10c14d4e99098c92a5d68ee55c7a92a3df095831a6933", size = 10928 }
858
+ wheels = [
859
+ { url = "https://files.pythonhosted.org/packages/dc/ec/ff0222b912fdff040a1908ea226f64ff5a1a384a4062a8714eb8b80ea5d9/types_croniter-5.0.1.20241205-py3-none-any.whl", hash = "sha256:e36e992d6a4600ddf510991a20d3273b888ecea5d77a19078348e00775ba54d3", size = 9375 },
860
+ ]
861
+
849
862
  [[package]]
850
863
  name = "types-jsonschema"
851
864
  version = "4.23.0.20240813"
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