jobflow 0.1.18__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 (37) hide show
  1. {jobflow-0.1.18/src/jobflow.egg-info → jobflow-0.2.0}/PKG-INFO +28 -26
  2. {jobflow-0.1.18 → jobflow-0.2.0}/pyproject.toml +27 -25
  3. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/core/flow.py +47 -4
  4. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/core/job.py +30 -14
  5. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/core/maker.py +1 -1
  6. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/core/reference.py +1 -1
  7. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/managers/local.py +8 -5
  8. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/settings.py +13 -2
  9. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/utils/graph.py +1 -1
  10. jobflow-0.2.0/src/jobflow/utils/log.py +41 -0
  11. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/utils/uuid.py +4 -1
  12. {jobflow-0.1.18 → jobflow-0.2.0/src/jobflow.egg-info}/PKG-INFO +28 -26
  13. jobflow-0.2.0/src/jobflow.egg-info/requires.txt +52 -0
  14. jobflow-0.1.18/src/jobflow/utils/log.py +0 -29
  15. jobflow-0.1.18/src/jobflow.egg-info/requires.txt +0 -52
  16. {jobflow-0.1.18 → jobflow-0.2.0}/LICENSE +0 -0
  17. {jobflow-0.1.18 → jobflow-0.2.0}/README.md +0 -0
  18. {jobflow-0.1.18 → jobflow-0.2.0}/setup.cfg +0 -0
  19. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/__init__.py +0 -0
  20. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/_version.py +0 -0
  21. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/core/__init__.py +0 -0
  22. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/core/schemas.py +0 -0
  23. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/core/state.py +0 -0
  24. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/core/store.py +0 -0
  25. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/managers/__init__.py +0 -0
  26. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/managers/fireworks.py +0 -0
  27. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/py.typed +0 -0
  28. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/utils/__init__.py +0 -0
  29. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/utils/dict_mods.py +0 -0
  30. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/utils/enum.py +0 -0
  31. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/utils/find.py +0 -0
  32. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow/utils/uid.py +0 -0
  33. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow.egg-info/SOURCES.txt +0 -0
  34. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow.egg-info/dependency_links.txt +0 -0
  35. {jobflow-0.1.18 → jobflow-0.2.0}/src/jobflow.egg-info/top_level.txt +0 -0
  36. {jobflow-0.1.18 → jobflow-0.2.0}/tests/test_settings.py +0 -0
  37. {jobflow-0.1.18 → jobflow-0.2.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: jobflow
3
- Version: 0.1.18
3
+ Version: 0.2.0
4
4
  Summary: jobflow is a library for writing computational workflows
5
5
  Author-email: Alex Ganose <a.ganose@imperial.ac.uk>
6
6
  License: modified BSD
@@ -17,11 +17,12 @@ Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python :: 3
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
21
22
  Classifier: Topic :: Database :: Front-Ends
22
23
  Classifier: Topic :: Other/Nonlisted Topic
23
24
  Classifier: Topic :: Scientific/Engineering
24
- Requires-Python: >=3.9
25
+ Requires-Python: >=3.10
25
26
  Description-Content-Type: text/markdown
26
27
  License-File: LICENSE
27
28
  Requires-Dist: PyYAML
@@ -34,39 +35,40 @@ Requires-Dist: pydash
34
35
  Provides-Extra: ulid
35
36
  Requires-Dist: python-ulid; extra == "ulid"
36
37
  Provides-Extra: docs
37
- Requires-Dist: autodoc_pydantic==2.1.0; extra == "docs"
38
- Requires-Dist: furo==2024.5.6; extra == "docs"
39
- Requires-Dist: ipython==8.26.0; extra == "docs"
40
- Requires-Dist: myst_parser==3.0.1; extra == "docs"
41
- Requires-Dist: nbsphinx==0.9.4; extra == "docs"
38
+ Requires-Dist: autodoc_pydantic==2.2.0; extra == "docs"
39
+ Requires-Dist: furo==2024.8.6; extra == "docs"
40
+ Requires-Dist: ipython==9.3.0; extra == "docs"
41
+ Requires-Dist: myst_parser==4.0.1; extra == "docs"
42
+ Requires-Dist: nbsphinx==0.9.7; extra == "docs"
42
43
  Requires-Dist: sphinx-copybutton==0.5.2; extra == "docs"
43
- Requires-Dist: sphinx==7.4.4; extra == "docs"
44
+ Requires-Dist: sphinx==8.1.3; extra == "docs"
44
45
  Provides-Extra: dev
45
46
  Requires-Dist: pre-commit>=2.12.1; extra == "dev"
46
47
  Requires-Dist: typing_extensions; python_version < "3.11" and extra == "dev"
47
48
  Provides-Extra: tests
48
- Requires-Dist: moto==4.2.13; extra == "tests"
49
- Requires-Dist: pytest-cov==5.0.0; extra == "tests"
50
- Requires-Dist: pytest==8.2.2; extra == "tests"
49
+ Requires-Dist: moto==5.1.5; extra == "tests"
50
+ Requires-Dist: pytest-cov==6.1.1; extra == "tests"
51
+ Requires-Dist: pytest==8.4.0; extra == "tests"
51
52
  Provides-Extra: vis
52
53
  Requires-Dist: matplotlib; extra == "vis"
53
54
  Requires-Dist: pydot; extra == "vis"
54
55
  Provides-Extra: fireworks
55
56
  Requires-Dist: FireWorks; extra == "fireworks"
56
57
  Provides-Extra: strict
57
- Requires-Dist: FireWorks==2.0.3; extra == "strict"
58
- Requires-Dist: PyYAML==6.0.1; extra == "strict"
59
- Requires-Dist: maggma==0.69.0; extra == "strict"
60
- Requires-Dist: matplotlib==3.9.1; extra == "strict"
61
- Requires-Dist: monty==2024.7.12; extra == "strict"
62
- Requires-Dist: moto==4.2.13; extra == "strict"
63
- Requires-Dist: networkx==3.2.1; extra == "strict"
64
- Requires-Dist: pydantic-settings==2.3.4; extra == "strict"
65
- Requires-Dist: pydantic==2.8.2; extra == "strict"
66
- Requires-Dist: pydash==8.0.1; extra == "strict"
67
- Requires-Dist: pydot==2.0.0; extra == "strict"
68
- Requires-Dist: python-ulid==2.7.0; extra == "strict"
69
- Requires-Dist: typing-extensions==4.12.2; extra == "strict"
58
+ Requires-Dist: FireWorks==2.0.4; extra == "strict"
59
+ Requires-Dist: PyYAML==6.0.2; extra == "strict"
60
+ Requires-Dist: maggma==0.71.5; extra == "strict"
61
+ Requires-Dist: matplotlib==3.10.3; extra == "strict"
62
+ Requires-Dist: monty==2025.3.3; extra == "strict"
63
+ Requires-Dist: moto==5.1.5; extra == "strict"
64
+ Requires-Dist: networkx==3.4.2; extra == "strict"
65
+ Requires-Dist: pydantic-settings==2.9.1; extra == "strict"
66
+ Requires-Dist: pydantic==2.11.5; extra == "strict"
67
+ Requires-Dist: pydash==8.0.5; extra == "strict"
68
+ Requires-Dist: pydot==4.0.0; extra == "strict"
69
+ Requires-Dist: python-ulid==3.0.0; extra == "strict"
70
+ Requires-Dist: typing-extensions==4.13.2; extra == "strict"
71
+ Dynamic: license-file
70
72
 
71
73
  <div align="center">
72
74
 
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["setuptools >= 42", "versioningit ~= 1.0", "wheel"]
2
+ requires = ["setuptools >= 42", "versioningit >= 1,< 4", "wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
@@ -19,12 +19,13 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3",
20
20
  "Programming Language :: Python :: 3.10",
21
21
  "Programming Language :: Python :: 3.11",
22
- "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
23
24
  "Topic :: Database :: Front-Ends",
24
25
  "Topic :: Other/Nonlisted Topic",
25
26
  "Topic :: Scientific/Engineering",
26
27
  ]
27
- requires-python = ">=3.9"
28
+ requires-python = ">=3.10"
28
29
  dependencies = [
29
30
  "PyYAML",
30
31
  "maggma>=0.57.0",
@@ -38,32 +39,32 @@ dependencies = [
38
39
  [project.optional-dependencies]
39
40
  ulid = ["python-ulid"]
40
41
  docs = [
41
- "autodoc_pydantic==2.1.0",
42
- "furo==2024.5.6",
43
- "ipython==8.26.0",
44
- "myst_parser==3.0.1",
45
- "nbsphinx==0.9.4",
42
+ "autodoc_pydantic==2.2.0",
43
+ "furo==2024.8.6",
44
+ "ipython==9.3.0",
45
+ "myst_parser==4.0.1",
46
+ "nbsphinx==0.9.7",
46
47
  "sphinx-copybutton==0.5.2",
47
- "sphinx==7.4.4",
48
+ "sphinx==8.1.3",
48
49
  ]
49
50
  dev = ["pre-commit>=2.12.1", "typing_extensions; python_version < '3.11'"]
50
- tests = ["moto==4.2.13", "pytest-cov==5.0.0", "pytest==8.2.2"]
51
+ tests = ["moto==5.1.5", "pytest-cov==6.1.1", "pytest==8.4.0"]
51
52
  vis = ["matplotlib", "pydot"]
52
53
  fireworks = ["FireWorks"]
53
54
  strict = [
54
- "FireWorks==2.0.3",
55
- "PyYAML==6.0.1",
56
- "maggma==0.69.0",
57
- "matplotlib==3.9.1",
58
- "monty==2024.7.12",
59
- "moto==4.2.13",
60
- "networkx==3.2.1",
61
- "pydantic-settings==2.3.4",
62
- "pydantic==2.8.2",
63
- "pydash==8.0.1",
64
- "pydot==2.0.0",
65
- "python-ulid==2.7.0",
66
- "typing-extensions==4.12.2",
55
+ "FireWorks==2.0.4",
56
+ "PyYAML==6.0.2",
57
+ "maggma==0.71.5",
58
+ "matplotlib==3.10.3",
59
+ "monty==2025.3.3",
60
+ "moto==5.1.5",
61
+ "networkx==3.4.2",
62
+ "pydantic-settings==2.9.1",
63
+ "pydantic==2.11.5",
64
+ "pydash==8.0.5",
65
+ "pydot==4.0.0",
66
+ "python-ulid==3.0.0",
67
+ "typing-extensions==4.13.2",
67
68
  ]
68
69
 
69
70
  [project.urls]
@@ -84,7 +85,7 @@ max-line-length = 88
84
85
  max-doc-length = 88
85
86
  select = "C, E, F, W, B"
86
87
  extend-ignore = "E203, W503, E501, F401, RST21"
87
- min-python-version = "3.9.0"
88
+ min-python-version = "3.10.0"
88
89
  docstring-convention = "numpy"
89
90
  rst-roles = "class, func, ref, obj"
90
91
 
@@ -121,6 +122,7 @@ exclude_lines = [
121
122
 
122
123
  [tool.ruff]
123
124
  target-version = "py39"
125
+ output-format = "concise"
124
126
 
125
127
  [tool.ruff.lint]
126
128
  select = [
@@ -171,7 +173,6 @@ ignore = [
171
173
  ]
172
174
  pydocstyle.convention = "numpy"
173
175
  isort.known-first-party = ["jobflow"]
174
- ignore-init-module-imports = true
175
176
 
176
177
  [tool.ruff.lint.per-file-ignores]
177
178
  # F401: unused import
@@ -180,3 +181,4 @@ ignore-init-module-imports = true
180
181
  # PLR2004: magic-value-comparison
181
182
  # PT004: pytest-missing-fixture-name-underscore
182
183
  "**/tests/*" = ["ANN", "ARG001", "D", "PLR2004", "PT004", "S101"]
184
+ "docs/tutorials/*" = ["D", "PLR2004"]
@@ -67,6 +67,11 @@ class Flow(MSONable):
67
67
  automatically when a flow is included in the jobs array of another flow.
68
68
  The object identified by one UUID of the list should be contained in objects
69
69
  identified by its subsequent elements.
70
+ metadata
71
+ A dictionary of information that will get stored in the Flow collection.
72
+ metadata_updates
73
+ A list of updates for the metadata that will be applied to any dynamically
74
+ generated sub Flow/Job.
70
75
 
71
76
  Raises
72
77
  ------
@@ -128,6 +133,8 @@ class Flow(MSONable):
128
133
  order: JobOrder = JobOrder.AUTO,
129
134
  uuid: str = None,
130
135
  hosts: list[str] = None,
136
+ metadata: dict[str, Any] = None,
137
+ metadata_updates: list[dict[str, Any]] = None,
131
138
  ):
132
139
  from jobflow.core.job import Job
133
140
 
@@ -141,6 +148,8 @@ class Flow(MSONable):
141
148
  self.order = order
142
149
  self.uuid = uuid
143
150
  self.hosts = hosts or []
151
+ self.metadata = metadata or {}
152
+ self.metadata_updates = metadata_updates or []
144
153
 
145
154
  self._jobs: tuple[Flow | Job, ...] = ()
146
155
  self.add_jobs(jobs)
@@ -190,7 +199,7 @@ class Flow(MSONable):
190
199
  if other not in self:
191
200
  raise ValueError(f"{other!r} not found in flow")
192
201
  new_flow = deepcopy(self)
193
- new_flow.jobs = tuple([job for job in new_flow if job != other])
202
+ new_flow.jobs = tuple(job for job in new_flow if job != other)
194
203
  return new_flow
195
204
 
196
205
  def __repr__(self, level: int = 0, prefix: str = "") -> str:
@@ -608,9 +617,10 @@ class Flow(MSONable):
608
617
  function_filter: Callable = None,
609
618
  dict_mod: bool = False,
610
619
  dynamic: bool = True,
620
+ callback_filter: Callable[[Flow | Job], bool] | None = None,
611
621
  ):
612
622
  """
613
- Update the metadata of all Jobs in the Flow.
623
+ Update the metadata of the Flow and/or its Jobs.
614
624
 
615
625
  Note that updates will be applied to jobs in nested Flow.
616
626
 
@@ -630,6 +640,10 @@ class Flow(MSONable):
630
640
  dynamic
631
641
  The updates will be propagated to Jobs/Flows dynamically generated at
632
642
  runtime.
643
+ callback_filter
644
+ A function that takes a Flow or Job instance and returns True if updates
645
+ should be applied to that instance. Allows for custom filtering logic.
646
+ Applies recursively to nested Flows and Jobs so best be specific.
633
647
 
634
648
  Examples
635
649
  --------
@@ -646,16 +660,45 @@ class Flow(MSONable):
646
660
  The ``metadata`` of both jobs could be updated as follows:
647
661
 
648
662
  >>> flow.update_metadata({"tag": "addition_job"})
663
+
664
+ Or using a callback filter to only update flows containing a specific maker:
665
+
666
+ >>> flow.update_metadata(
667
+ ... {"material_id": 42},
668
+ ... callback_filter=lambda flow: SomeMaker in map(type, flow)
669
+ ... and flow.name == "flow name"
670
+ ... )
649
671
  """
650
- for job in self:
651
- job.update_metadata(
672
+ from jobflow.utils.dict_mods import apply_mod
673
+
674
+ for job_or_flow in self:
675
+ job_or_flow.update_metadata(
652
676
  update,
653
677
  name_filter=name_filter,
654
678
  function_filter=function_filter,
655
679
  dict_mod=dict_mod,
656
680
  dynamic=dynamic,
681
+ callback_filter=callback_filter,
657
682
  )
658
683
 
684
+ if callback_filter is not None and callback_filter(self) is False:
685
+ return
686
+
687
+ if dict_mod:
688
+ apply_mod(update, self.metadata)
689
+ else:
690
+ self.metadata.update(update)
691
+
692
+ if dynamic:
693
+ dict_input = {
694
+ "update": update,
695
+ "name_filter": name_filter,
696
+ "function_filter": function_filter,
697
+ "dict_mod": dict_mod,
698
+ "callback_filter": callback_filter,
699
+ }
700
+ self.metadata_updates.append(dict_input)
701
+
659
702
  def update_config(
660
703
  self,
661
704
  config: jobflow.JobConfig | dict,
@@ -69,17 +69,22 @@ class JobConfig(MSONable):
69
69
 
70
70
 
71
71
  @overload
72
- def job(method: Callable = None) -> Callable[..., Job]:
72
+ def job(method: Callable | None = None) -> Callable[..., Job]:
73
73
  pass
74
74
 
75
75
 
76
76
  @overload
77
- def job(method: Callable = None, **job_kwargs) -> Callable[..., Callable[..., Job]]:
77
+ def job(method: Callable, **job_kwargs) -> Callable[..., Job]:
78
+ pass
79
+
80
+
81
+ @overload
82
+ def job(method: None = None, **job_kwargs) -> Callable[..., Callable[..., Job]]:
78
83
  pass
79
84
 
80
85
 
81
86
  def job(
82
- method: Callable = None, **job_kwargs
87
+ method: Callable | None = None, **job_kwargs
83
88
  ) -> Callable[..., Job] | Callable[..., Callable[..., Job]]:
84
89
  """
85
90
  Wrap a function to produce a :obj:`Job`.
@@ -292,12 +297,12 @@ class Job(MSONable):
292
297
  --------
293
298
  Builtin functions such as :obj:`print` can be specified.
294
299
 
295
- >>> print_task = Job(function=print, args=("I am a job", ))
300
+ >>> print_task = Job(function=print, function_args=("I am a job", ))
296
301
 
297
302
  Or other functions of the Python standard library.
298
303
 
299
304
  >>> import os
300
- >>> Job(function=os.path.join, args=("folder", "filename.txt"))
305
+ >>> Job(function=os.path.join, function_args=("folder", "filename.txt"))
301
306
 
302
307
  To use custom functions, the functions should be importable (i.e. not
303
308
  defined in another function). For example, if the following function is defined
@@ -305,7 +310,7 @@ class Job(MSONable):
305
310
 
306
311
  >>> def add(a, b):
307
312
  ... return a + b
308
- >>> add_job = Job(function=add, args=(1, 2))
313
+ >>> add_job = Job(function=add, function_args=(1, 2))
309
314
 
310
315
  More details are given in the :obj:`job` decorator docstring.
311
316
 
@@ -338,7 +343,6 @@ class Job(MSONable):
338
343
  function_args = () if function_args is None else function_args
339
344
  function_kwargs = {} if function_kwargs is None else function_kwargs
340
345
  uuid = suid() if uuid is None else uuid
341
- metadata = {} if metadata is None else metadata
342
346
  config = JobConfig() if config is None else config
343
347
 
344
348
  # make a deep copy of the function (means makers do not share the same instance)
@@ -349,7 +353,7 @@ class Job(MSONable):
349
353
  self.uuid = uuid
350
354
  self.index = index
351
355
  self.name = name
352
- self.metadata = metadata
356
+ self.metadata = metadata or {}
353
357
  self.config = config
354
358
  self.hosts = hosts or []
355
359
  self.metadata_updates = metadata_updates or []
@@ -922,6 +926,7 @@ class Job(MSONable):
922
926
  function_filter: Callable = None,
923
927
  dict_mod: bool = False,
924
928
  dynamic: bool = True,
929
+ callback_filter: Callable[[jobflow.Flow | Job], bool] | None = None,
925
930
  ):
926
931
  """
927
932
  Update the metadata of the job.
@@ -945,6 +950,9 @@ class Job(MSONable):
945
950
  dynamic
946
951
  The updates will be propagated to Jobs/Flows dynamically generated at
947
952
  runtime.
953
+ callback_filter
954
+ A function that takes a Flow or Job instance and returns True if updates
955
+ should be applied to that instance. Allows for custom filtering logic.
948
956
 
949
957
  Examples
950
958
  --------
@@ -963,11 +971,16 @@ class Job(MSONable):
963
971
  will not only set the `example` metadata to the `test_job`, but also to all the
964
972
  new Jobs that will be generated at runtime by the ExampleMaker.
965
973
 
966
- `update_metadata` can be called multiple times with different `name_filter` or
967
- `function_filter` to control which Jobs will be updated.
974
+ `update_metadata` can be called multiple times with different filters to control
975
+ which Jobs will be updated. For example, using a callback filter:
968
976
 
969
- At variance, if `dynamic` is set to `False` the `example` metadata will only be
970
- added to the `test_job` and not to the generated Jobs.
977
+ >>> test_job.update_metadata(
978
+ ... {"material_id": 42},
979
+ ... callback_filter=lambda job: isinstance(job.maker, SomeMaker)
980
+ ... )
981
+
982
+ At variance, if `dynamic` is set to `False` the metadata will only be
983
+ added to the filtered Jobs and not to any generated Jobs.
971
984
  """
972
985
  from jobflow.utils.dict_mods import apply_mod
973
986
 
@@ -977,6 +990,7 @@ class Job(MSONable):
977
990
  "name_filter": name_filter,
978
991
  "function_filter": function_filter,
979
992
  "dict_mod": dict_mod,
993
+ "callback_filter": callback_filter,
980
994
  }
981
995
  self.metadata_updates.append(dict_input)
982
996
 
@@ -984,7 +998,6 @@ class Job(MSONable):
984
998
  function_filter = getattr(function_filter, "__wrapped__", function_filter)
985
999
  function = getattr(self.function, "__wrapped__", self.function)
986
1000
 
987
- # if function_filter is not None and function_filter != self.function:
988
1001
  if function_filter is not None and function_filter != function:
989
1002
  return
990
1003
 
@@ -993,6 +1006,9 @@ class Job(MSONable):
993
1006
  ):
994
1007
  return
995
1008
 
1009
+ if callback_filter is not None and callback_filter(self) is False:
1010
+ return
1011
+
996
1012
  # if we get to here then we pass all the filters
997
1013
  if dict_mod:
998
1014
  apply_mod(update, self.metadata)
@@ -1249,7 +1265,7 @@ class Response(typing.Generic[T]):
1249
1265
  job_returns.output = apply_schema(job_returns.output, output_schema)
1250
1266
 
1251
1267
  job_returns.job_dir = job_dir
1252
- return cast(Self, job_returns)
1268
+ return cast("Self", job_returns)
1253
1269
 
1254
1270
  if isinstance(job_returns, (list, tuple)):
1255
1271
  # check that a Response object is not given as one of many outputs
@@ -261,7 +261,7 @@ def recursive_call(
261
261
 
262
262
  if isinstance(class_filter, Maker):
263
263
  # Maker instance supplied rather than a Maker class
264
- class_filter = class_filter.__class__
264
+ class_filter = class_filter.__class__ # type: ignore[assignment]
265
265
 
266
266
  def _filter(nested_obj: Maker):
267
267
  # Filter the Maker object
@@ -513,7 +513,7 @@ def validate_schema_access(
513
513
  the bool is ``True`` if the schema access was valid.
514
514
  The BaseModel class associated with the item, if any.
515
515
  """
516
- schema_dict = schema.schema()
516
+ schema_dict = schema.model_json_schema()
517
517
  if item not in schema_dict["properties"]:
518
518
  raise AttributeError(f"{schema.__name__} does not have attribute '{item}'.")
519
519
 
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
15
15
 
16
16
  def run_locally(
17
17
  flow: jobflow.Flow | jobflow.Job | list[jobflow.Job],
18
- log: bool = True,
18
+ log: bool | str = True,
19
19
  store: jobflow.JobStore | None = None,
20
20
  create_folders: bool = False,
21
21
  root_dir: str | Path | None = None,
@@ -30,8 +30,11 @@ def run_locally(
30
30
  ----------
31
31
  flow : Flow | Job | list[Job]
32
32
  A job or flow.
33
- log : bool
34
- Whether to print log messages.
33
+ log : bool | str
34
+ Controls logging. Defaults to True. Can be:
35
+ - False: disable logging
36
+ - True: use default logging format (read from ~/.jobflow.yaml)
37
+ - str: custom logging format string (e.g. "%(message)s" for more concise output)
35
38
  store : JobStore
36
39
  A job store. If a job store is not specified then
37
40
  :obj:`JobflowSettings.JOB_STORE` will be used. By default this is a maggma
@@ -46,7 +49,7 @@ def run_locally(
46
49
  Raise an error if the flow was not executed successfully.
47
50
  allow_external_references : bool
48
51
  If False all the references to other outputs should be from other Jobs
49
- of the Flow.
52
+ of the same Flow.
50
53
  raise_immediately : bool
51
54
  If True, raise an exception immediately if a job fails. If False, continue
52
55
  running the flow and only raise an exception at the end if the flow did not
@@ -77,7 +80,7 @@ def run_locally(
77
80
  store.connect()
78
81
 
79
82
  if log:
80
- initialize_logger()
83
+ initialize_logger(fmt=log if isinstance(log, str) else "")
81
84
 
82
85
  flow = get_flow(flow, allow_external_references=allow_external_references)
83
86
 
@@ -11,6 +11,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
11
11
  from jobflow import JobStore
12
12
 
13
13
  DEFAULT_CONFIG_FILE_PATH = Path("~/.jobflow.yaml").expanduser().as_posix()
14
+ DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)s %(message)s"
15
+ DEFAULT_DIRECTORY_FORMAT = "%Y-%m-%d-%H-%M-%S-%f"
14
16
 
15
17
 
16
18
  def _default_additional_store():
@@ -28,7 +30,7 @@ class JobflowSettings(BaseSettings):
28
30
  """
29
31
  Settings for jobflow.
30
32
 
31
- The default way to modify these is to modify ~/.jobflow.yaml. Alternatively,
33
+ The default way to modify these is to create a ~/.jobflow.yaml. Alternatively,
32
34
  the environment variable ``JOBFLOW_CONFIG_FILE`` can be set to point to a yaml file
33
35
  with jobflow settings.
34
36
 
@@ -114,9 +116,18 @@ class JobflowSettings(BaseSettings):
114
116
  "accepted formats.",
115
117
  )
116
118
  DIRECTORY_FORMAT: str = Field(
117
- "%Y-%m-%d-%H-%M-%S-%f",
119
+ DEFAULT_DIRECTORY_FORMAT,
118
120
  description="Date stamp format used to create directories",
119
121
  )
122
+ LOG_FORMAT: str = Field(
123
+ DEFAULT_LOG_FORMAT,
124
+ description="""Logging format string. Common format codes:
125
+ - %(message)s - The logged message
126
+ - %(asctime)s - Human-readable time
127
+ - %(levelname)s - DEBUG, INFO, WARNING, ERROR, or CRITICAL
128
+ - %(name)s - Logger name
129
+ See Python logging documentation for more format codes.""",
130
+ )
120
131
 
121
132
  UID_TYPE: str = Field(
122
133
  "uuid4", description="Type of unique identifier to use to track jobs. "
@@ -170,7 +170,7 @@ def to_pydot(flow: jobflow.Flow):
170
170
  if isinstance(job, Flow):
171
171
  add_cluster(job, cluster)
172
172
  else:
173
- cluster.add_node(pydot_graph.get_node(f'"{job.uuid}"')[0])
173
+ cluster.add_node(pydot_graph.get_node(f"{job.uuid}")[0])
174
174
 
175
175
  outer_graph.add_subgraph(cluster)
176
176
 
@@ -0,0 +1,41 @@
1
+ """Tools for logging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+
8
+ def initialize_logger(level: int = logging.INFO, fmt: str = "") -> logging.Logger:
9
+ """Initialize the default logger.
10
+
11
+ Parameters
12
+ ----------
13
+ level
14
+ The log level.
15
+ fmt
16
+ Custom logging format string. Defaults to JobflowSettings.LOG_FORMAT.
17
+ Common format codes:
18
+ - %(message)s - The logged message
19
+ - %(asctime)s - Human-readable time
20
+ - %(levelname)s - DEBUG, INFO, WARNING, ERROR, or CRITICAL
21
+ See Python logging documentation for more format codes.
22
+
23
+ Returns
24
+ -------
25
+ Logger
26
+ A logging instance with customized formatter and handlers.
27
+ """
28
+ import sys
29
+
30
+ from jobflow import SETTINGS
31
+
32
+ log = logging.getLogger("jobflow")
33
+ log.setLevel(level)
34
+ log.handlers = [] # reset logging handlers if they already exist
35
+
36
+ formatter = logging.Formatter(fmt or SETTINGS.LOG_FORMAT)
37
+
38
+ screen_handler = logging.StreamHandler(stream=sys.stdout)
39
+ screen_handler.setFormatter(formatter)
40
+ log.addHandler(screen_handler)
41
+ return log
@@ -4,7 +4,10 @@ from monty.dev import deprecated
4
4
 
5
5
 
6
6
  @deprecated(
7
- message="The UUID system will be replace with UID that contains both UUID and ULID."
7
+ message=(
8
+ "The UUID system will be replaced with a UID that "
9
+ "contains both the UUID and ULID."
10
+ )
8
11
  )
9
12
  def suuid() -> str:
10
13
  """
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: jobflow
3
- Version: 0.1.18
3
+ Version: 0.2.0
4
4
  Summary: jobflow is a library for writing computational workflows
5
5
  Author-email: Alex Ganose <a.ganose@imperial.ac.uk>
6
6
  License: modified BSD
@@ -17,11 +17,12 @@ Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python :: 3
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
21
22
  Classifier: Topic :: Database :: Front-Ends
22
23
  Classifier: Topic :: Other/Nonlisted Topic
23
24
  Classifier: Topic :: Scientific/Engineering
24
- Requires-Python: >=3.9
25
+ Requires-Python: >=3.10
25
26
  Description-Content-Type: text/markdown
26
27
  License-File: LICENSE
27
28
  Requires-Dist: PyYAML
@@ -34,39 +35,40 @@ Requires-Dist: pydash
34
35
  Provides-Extra: ulid
35
36
  Requires-Dist: python-ulid; extra == "ulid"
36
37
  Provides-Extra: docs
37
- Requires-Dist: autodoc_pydantic==2.1.0; extra == "docs"
38
- Requires-Dist: furo==2024.5.6; extra == "docs"
39
- Requires-Dist: ipython==8.26.0; extra == "docs"
40
- Requires-Dist: myst_parser==3.0.1; extra == "docs"
41
- Requires-Dist: nbsphinx==0.9.4; extra == "docs"
38
+ Requires-Dist: autodoc_pydantic==2.2.0; extra == "docs"
39
+ Requires-Dist: furo==2024.8.6; extra == "docs"
40
+ Requires-Dist: ipython==9.3.0; extra == "docs"
41
+ Requires-Dist: myst_parser==4.0.1; extra == "docs"
42
+ Requires-Dist: nbsphinx==0.9.7; extra == "docs"
42
43
  Requires-Dist: sphinx-copybutton==0.5.2; extra == "docs"
43
- Requires-Dist: sphinx==7.4.4; extra == "docs"
44
+ Requires-Dist: sphinx==8.1.3; extra == "docs"
44
45
  Provides-Extra: dev
45
46
  Requires-Dist: pre-commit>=2.12.1; extra == "dev"
46
47
  Requires-Dist: typing_extensions; python_version < "3.11" and extra == "dev"
47
48
  Provides-Extra: tests
48
- Requires-Dist: moto==4.2.13; extra == "tests"
49
- Requires-Dist: pytest-cov==5.0.0; extra == "tests"
50
- Requires-Dist: pytest==8.2.2; extra == "tests"
49
+ Requires-Dist: moto==5.1.5; extra == "tests"
50
+ Requires-Dist: pytest-cov==6.1.1; extra == "tests"
51
+ Requires-Dist: pytest==8.4.0; extra == "tests"
51
52
  Provides-Extra: vis
52
53
  Requires-Dist: matplotlib; extra == "vis"
53
54
  Requires-Dist: pydot; extra == "vis"
54
55
  Provides-Extra: fireworks
55
56
  Requires-Dist: FireWorks; extra == "fireworks"
56
57
  Provides-Extra: strict
57
- Requires-Dist: FireWorks==2.0.3; extra == "strict"
58
- Requires-Dist: PyYAML==6.0.1; extra == "strict"
59
- Requires-Dist: maggma==0.69.0; extra == "strict"
60
- Requires-Dist: matplotlib==3.9.1; extra == "strict"
61
- Requires-Dist: monty==2024.7.12; extra == "strict"
62
- Requires-Dist: moto==4.2.13; extra == "strict"
63
- Requires-Dist: networkx==3.2.1; extra == "strict"
64
- Requires-Dist: pydantic-settings==2.3.4; extra == "strict"
65
- Requires-Dist: pydantic==2.8.2; extra == "strict"
66
- Requires-Dist: pydash==8.0.1; extra == "strict"
67
- Requires-Dist: pydot==2.0.0; extra == "strict"
68
- Requires-Dist: python-ulid==2.7.0; extra == "strict"
69
- Requires-Dist: typing-extensions==4.12.2; extra == "strict"
58
+ Requires-Dist: FireWorks==2.0.4; extra == "strict"
59
+ Requires-Dist: PyYAML==6.0.2; extra == "strict"
60
+ Requires-Dist: maggma==0.71.5; extra == "strict"
61
+ Requires-Dist: matplotlib==3.10.3; extra == "strict"
62
+ Requires-Dist: monty==2025.3.3; extra == "strict"
63
+ Requires-Dist: moto==5.1.5; extra == "strict"
64
+ Requires-Dist: networkx==3.4.2; extra == "strict"
65
+ Requires-Dist: pydantic-settings==2.9.1; extra == "strict"
66
+ Requires-Dist: pydantic==2.11.5; extra == "strict"
67
+ Requires-Dist: pydash==8.0.5; extra == "strict"
68
+ Requires-Dist: pydot==4.0.0; extra == "strict"
69
+ Requires-Dist: python-ulid==3.0.0; extra == "strict"
70
+ Requires-Dist: typing-extensions==4.13.2; extra == "strict"
71
+ Dynamic: license-file
70
72
 
71
73
  <div align="center">
72
74
 
@@ -0,0 +1,52 @@
1
+ PyYAML
2
+ maggma>=0.57.0
3
+ monty>=2023.9.25
4
+ networkx
5
+ pydantic-settings>=2.0.3
6
+ pydantic>=2.0.1
7
+ pydash
8
+
9
+ [dev]
10
+ pre-commit>=2.12.1
11
+
12
+ [dev:python_version < "3.11"]
13
+ typing_extensions
14
+
15
+ [docs]
16
+ autodoc_pydantic==2.2.0
17
+ furo==2024.8.6
18
+ ipython==9.3.0
19
+ myst_parser==4.0.1
20
+ nbsphinx==0.9.7
21
+ sphinx-copybutton==0.5.2
22
+ sphinx==8.1.3
23
+
24
+ [fireworks]
25
+ FireWorks
26
+
27
+ [strict]
28
+ FireWorks==2.0.4
29
+ PyYAML==6.0.2
30
+ maggma==0.71.5
31
+ matplotlib==3.10.3
32
+ monty==2025.3.3
33
+ moto==5.1.5
34
+ networkx==3.4.2
35
+ pydantic-settings==2.9.1
36
+ pydantic==2.11.5
37
+ pydash==8.0.5
38
+ pydot==4.0.0
39
+ python-ulid==3.0.0
40
+ typing-extensions==4.13.2
41
+
42
+ [tests]
43
+ moto==5.1.5
44
+ pytest-cov==6.1.1
45
+ pytest==8.4.0
46
+
47
+ [ulid]
48
+ python-ulid
49
+
50
+ [vis]
51
+ matplotlib
52
+ pydot
@@ -1,29 +0,0 @@
1
- """Tools for logging."""
2
-
3
- import logging
4
-
5
-
6
- def initialize_logger(level: int = logging.INFO) -> logging.Logger:
7
- """Initialize the default logger.
8
-
9
- Parameters
10
- ----------
11
- level
12
- The log level.
13
-
14
- Returns
15
- -------
16
- Logger
17
- A logging instance with customized formatter and handlers.
18
- """
19
- import sys
20
-
21
- log = logging.getLogger("jobflow")
22
- log.setLevel(level)
23
- log.handlers = [] # reset logging handlers if they already exist
24
-
25
- fmt = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
26
- screen_handler = logging.StreamHandler(stream=sys.stdout)
27
- screen_handler.setFormatter(fmt)
28
- log.addHandler(screen_handler)
29
- return log
@@ -1,52 +0,0 @@
1
- PyYAML
2
- maggma>=0.57.0
3
- monty>=2023.9.25
4
- networkx
5
- pydantic-settings>=2.0.3
6
- pydantic>=2.0.1
7
- pydash
8
-
9
- [dev]
10
- pre-commit>=2.12.1
11
-
12
- [dev:python_version < "3.11"]
13
- typing_extensions
14
-
15
- [docs]
16
- autodoc_pydantic==2.1.0
17
- furo==2024.5.6
18
- ipython==8.26.0
19
- myst_parser==3.0.1
20
- nbsphinx==0.9.4
21
- sphinx-copybutton==0.5.2
22
- sphinx==7.4.4
23
-
24
- [fireworks]
25
- FireWorks
26
-
27
- [strict]
28
- FireWorks==2.0.3
29
- PyYAML==6.0.1
30
- maggma==0.69.0
31
- matplotlib==3.9.1
32
- monty==2024.7.12
33
- moto==4.2.13
34
- networkx==3.2.1
35
- pydantic-settings==2.3.4
36
- pydantic==2.8.2
37
- pydash==8.0.1
38
- pydot==2.0.0
39
- python-ulid==2.7.0
40
- typing-extensions==4.12.2
41
-
42
- [tests]
43
- moto==4.2.13
44
- pytest-cov==5.0.0
45
- pytest==8.2.2
46
-
47
- [ulid]
48
- python-ulid
49
-
50
- [vis]
51
- matplotlib
52
- pydot
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes