dkist-processing-core 4.0.0__tar.gz → 4.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 (52) hide show
  1. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/CHANGELOG.rst +23 -0
  2. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/PKG-INFO +8 -1
  3. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/README.rst +7 -0
  4. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/__init__.py +4 -4
  5. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/build_utils.py +25 -21
  6. dkist-processing-core-4.0.0/dkist_processing_core/_node.py → dkist-processing-core-4.2.0/dkist_processing_core/node.py +18 -13
  7. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/task.py +7 -6
  8. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/conftest.py +2 -2
  9. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/test_failure_callback.py +5 -5
  10. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/test_node.py +7 -7
  11. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/test_workflow.py +45 -1
  12. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/workflow.py +48 -32
  13. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core.egg-info/PKG-INFO +8 -1
  14. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core.egg-info/SOURCES.txt +2 -2
  15. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core.egg-info/requires.txt +2 -1
  16. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/pyproject.toml +1 -0
  17. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/setup.cfg +2 -1
  18. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/.gitignore +0 -0
  19. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/.pre-commit-config.yaml +0 -0
  20. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/.readthedocs.yml +0 -0
  21. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/bitbucket-pipelines.yml +0 -0
  22. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/changelog/.gitempty +0 -0
  23. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/check_changelog_updated.sh +0 -0
  24. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/config.py +0 -0
  25. /dkist-processing-core-4.0.0/dkist_processing_core/_failure_callback.py → /dkist-processing-core-4.2.0/dkist_processing_core/failure_callback.py +0 -0
  26. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/resource_queue.py +0 -0
  27. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/__init__.py +0 -0
  28. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/invalid_workflow_cyclic/__init__.py +0 -0
  29. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/invalid_workflow_cyclic/workflow.py +0 -0
  30. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/invalid_workflow_for_docker_multi_category/__init__.py +0 -0
  31. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/invalid_workflow_for_docker_multi_category/workflow.py +0 -0
  32. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/task_example.py +0 -0
  33. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/test_build_utils.py +0 -0
  34. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/test_export.py +0 -0
  35. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/test_task.py +0 -0
  36. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/valid_workflow_package/__init__.py +0 -0
  37. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/valid_workflow_package/workflow.py +0 -0
  38. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/zero_node_workflow_package/__init__.py +0 -0
  39. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core/tests/zero_node_workflow_package/workflow.py +0 -0
  40. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core.egg-info/dependency_links.txt +0 -0
  41. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/dkist_processing_core.egg-info/top_level.txt +0 -0
  42. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/docs/Makefile +0 -0
  43. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/docs/auto-proc-concept-model.png +0 -0
  44. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/docs/auto_proc_brick.png +0 -0
  45. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/docs/automated-processing-deployed.png +0 -0
  46. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/docs/changelog.rst +0 -0
  47. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/docs/conf.py +0 -0
  48. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/docs/index.rst +0 -0
  49. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/docs/make.bat +0 -0
  50. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/docs/requirements.txt +0 -0
  51. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/licenses/LICENSE.rst +0 -0
  52. {dkist-processing-core-4.0.0 → dkist-processing-core-4.2.0}/setup.py +0 -0
@@ -1,3 +1,26 @@
1
+ v4.2.0 (2024-09-27)
2
+ ===================
3
+
4
+ Misc
5
+ ----
6
+
7
+ - Fixing deprecation warnings in pkg_resources. (`#39 <https://bitbucket.org/dkistdc/dkist-processing-core/pull-requests/39>`__)
8
+ - Utility for generating the name of a workflow is part of the public API. (`#40 <https://bitbucket.org/dkistdc/dkist-processing-core/pull-requests/40>`__)
9
+ - Upgrade to airflow 2.10.2. (`#41 <https://bitbucket.org/dkistdc/dkist-processing-core/pull-requests/41>`__)
10
+
11
+
12
+ v4.1.0 (2024-07-01)
13
+ ===================
14
+
15
+ Misc
16
+ ----
17
+
18
+ - Add utility for generating the name of a workflow. (`#35 <https://bitbucket.org/dkistdc/dkist-processing-core/pull-requests/35>`__)
19
+ - Update the instructions for development to include the dependency on rabbitmq and docker. (`#36 <https://bitbucket.org/dkistdc/dkist-processing-core/pull-requests/36>`__)
20
+ - Make private methods public when we want them to show up in the ReadTheDocs documentation. (`#37 <https://bitbucket.org/dkistdc/dkist-processing-core/pull-requests/37>`__)
21
+ - Upgrade airflow to version 2.9.2. (`#38 <https://bitbucket.org/dkistdc/dkist-processing-core/pull-requests/38>`__)
22
+
23
+
1
24
  v4.0.0 (2024-06-03)
2
25
  ===================
3
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dkist-processing-core
3
- Version: 4.0.0
3
+ Version: 4.2.0
4
4
  Summary: Abstraction layer that is used by the DKIST Science Data Processing pipelines to process DKIST data using Apache Airflow.
5
5
  Home-page: https://bitbucket.org/dkistdc/dkist-processing-core/src/main/
6
6
  Author: NSO / AURA
@@ -150,12 +150,19 @@ Environment Variables
150
150
 
151
151
  Development
152
152
  -----------
153
+ A prerequisite for test execution is a running instance of rabbitmq and docker on the local machine.
154
+ For RabbitMQ the tests will use the default guest/guest credentials and a host ip of 127.0.0.1 and port of 5672 to connect to the broker.
155
+ Getting docker set up varies by system, but the tests will use the default unix socket for the docker daemon.
156
+
157
+ To run the tests locally, clone the repository and install the package in editable mode with the test extras.
158
+
153
159
  .. code-block:: bash
154
160
 
155
161
  git clone git@bitbucket.org:dkistdc/dkist-processing-core.git
156
162
  cd dkist-processing-core
157
163
  pre-commit install
158
164
  pip install -e .[test]
165
+ # RabbitMQ and Docker needs to be running
159
166
  pytest -v --cov dkist_processing_core
160
167
 
161
168
  Changelog
@@ -134,12 +134,19 @@ Environment Variables
134
134
 
135
135
  Development
136
136
  -----------
137
+ A prerequisite for test execution is a running instance of rabbitmq and docker on the local machine.
138
+ For RabbitMQ the tests will use the default guest/guest credentials and a host ip of 127.0.0.1 and port of 5672 to connect to the broker.
139
+ Getting docker set up varies by system, but the tests will use the default unix socket for the docker daemon.
140
+
141
+ To run the tests locally, clone the repository and install the package in editable mode with the test extras.
142
+
137
143
  .. code-block:: bash
138
144
 
139
145
  git clone git@bitbucket.org:dkistdc/dkist-processing-core.git
140
146
  cd dkist-processing-core
141
147
  pre-commit install
142
148
  pip install -e .[test]
149
+ # RabbitMQ and Docker needs to be running
143
150
  pytest -v --cov dkist_processing_core
144
151
 
145
152
  Changelog
@@ -1,13 +1,13 @@
1
1
  """Package-level setup information."""
2
- from pkg_resources import DistributionNotFound
3
- from pkg_resources import get_distribution
2
+ from importlib.metadata import PackageNotFoundError
3
+ from importlib.metadata import version
4
4
 
5
5
  from dkist_processing_core.resource_queue import ResourceQueue
6
6
  from dkist_processing_core.task import TaskBase
7
7
  from dkist_processing_core.workflow import Workflow
8
8
 
9
9
  try:
10
- __version__ = get_distribution(__name__).version
11
- except DistributionNotFound:
10
+ __version__ = version(distribution_name=__name__)
11
+ except PackageNotFoundError:
12
12
  # package is not installed
13
13
  __version__ = "unknown"
@@ -16,7 +16,7 @@ def validate_workflows(workflow_package: ModuleType, export_path: Path | None =
16
16
  rm_export_path_after_test = not bool(export_path)
17
17
  if export_path is None:
18
18
  export_path = Path("export/")
19
- workflows = _extract_workflows_from_package(workflow_package)
19
+ workflows = extract_workflows_from_package(workflow_package)
20
20
  try:
21
21
  _validate_workflows(workflows, export_path)
22
22
  finally:
@@ -24,21 +24,31 @@ def validate_workflows(workflow_package: ModuleType, export_path: Path | None =
24
24
  rmtree(export_path)
25
25
 
26
26
 
27
+ def _validate_workflows(workflows: list[Workflow], export_path: Path) -> None:
28
+ """Validate workflows by ensuring their exported version compiles as python and that there is at least one node."""
29
+ for w in workflows:
30
+ workflow_py = w.export_dag(path=export_path)
31
+ with workflow_py.open(mode="r") as f:
32
+ compile(f.read(), filename=f"{workflow_py.stem}.pyc", mode="exec")
33
+ if len(w.nodes) == 0:
34
+ raise ValueError(f"Workflow {w.workflow_name} has 0 nodes.")
35
+
36
+
27
37
  def export_dags(workflow_package: ModuleType, path: str | Path) -> list[Path]:
28
38
  """Export Airflow DAG files."""
29
- return [w.export_dag(path=path) for w in _extract_workflows_from_package(workflow_package)]
39
+ return [w.export_dag(path=path) for w in extract_workflows_from_package(workflow_package)]
30
40
 
31
41
 
32
42
  def export_notebooks(workflow_package: ModuleType, path: str | Path) -> list[Path]:
33
43
  """Export Jupyter Notebook files."""
34
- return [w.export_notebook(path=path) for w in _extract_workflows_from_package(workflow_package)]
44
+ return [w.export_notebook(path=path) for w in extract_workflows_from_package(workflow_package)]
35
45
 
36
46
 
37
47
  def export_notebook_dockerfile(workflow_package: ModuleType, path: str | Path) -> Path:
38
48
  """Export a dockerfile to containerize notebooks."""
39
49
  path = Path(path)
40
50
  notebook_paths = export_notebooks(workflow_package=workflow_package, path=path)
41
- category = _extract_category_from_workflows(workflow_package=workflow_package)
51
+ category = extract_category_from_workflows(workflow_package=workflow_package)
42
52
  dockerfile = NotebookDockerfile(notebook_paths=notebook_paths, category=category)
43
53
  dockerfile_path = Path("Dockerfile")
44
54
  dockerfile_path.touch(exist_ok=False)
@@ -47,8 +57,9 @@ def export_notebook_dockerfile(workflow_package: ModuleType, path: str | Path) -
47
57
  return dockerfile_path
48
58
 
49
59
 
50
- def _extract_category_from_workflows(workflow_package: ModuleType) -> str:
51
- workflows = _extract_workflows_from_package(workflow_package)
60
+ def extract_category_from_workflows(workflow_package: ModuleType) -> str:
61
+ """Extract the category from the workflows in the package to provide a unique category for the dockerfile."""
62
+ workflows = extract_workflows_from_package(workflow_package)
52
63
  categories = {w.category for w in workflows}
53
64
  if len(categories) > 1:
54
65
  raise ValueError(
@@ -57,12 +68,14 @@ def _extract_category_from_workflows(workflow_package: ModuleType) -> str:
57
68
  return categories.pop()
58
69
 
59
70
 
60
- def _extract_workflows_from_package(workflow_package: ModuleType) -> list[Workflow]:
61
- return _extract_objects_from_package_by_type(workflow_package, Workflow)
71
+ def extract_workflows_from_package(workflow_package: ModuleType) -> list[Workflow]:
72
+ """Extract all the Workflow objects from a package."""
73
+ return extract_objects_from_package_by_type(workflow_package, Workflow)
62
74
 
63
75
 
64
- def _extract_objects_from_package_by_type(package: ModuleType, object_type: type) -> list:
65
- modules = _parse_unprotected_modules_names_from_package(package)
76
+ def extract_objects_from_package_by_type(package: ModuleType, object_type: type) -> list:
77
+ """Extract all objects in public modules of a given type from a package."""
78
+ modules = parse_unprotected_modules_names_from_package(package)
66
79
  objects = []
67
80
  for module in modules:
68
81
  imported_module = importlib.import_module(f".{module}", package.__name__)
@@ -70,21 +83,12 @@ def _extract_objects_from_package_by_type(package: ModuleType, object_type: type
70
83
  return objects
71
84
 
72
85
 
73
- def _parse_unprotected_modules_names_from_package(package: ModuleType) -> list[str]:
86
+ def parse_unprotected_modules_names_from_package(package: ModuleType) -> list[str]:
87
+ """Parse the names of all modules in a package that are not private i.e. don't begin with an underscore."""
74
88
  package_path = Path(package.__path__[0])
75
89
  return [m.stem for m in package_path.glob("[!_]*.py")]
76
90
 
77
91
 
78
- def _validate_workflows(workflows: list[Workflow], export_path: Path) -> None:
79
- """Validate workflows by ensuring their exported version compiles as python and that there is at least one node."""
80
- for w in workflows:
81
- workflow_py = w.export_dag(path=export_path)
82
- with workflow_py.open(mode="r") as f:
83
- compile(f.read(), filename=f"{workflow_py.stem}.pyc", mode="exec")
84
- if len(w.nodes) == 0:
85
- raise ValueError(f"Workflow {w.workflow_name} has 0 nodes.")
86
-
87
-
88
92
  class NotebookDockerfile:
89
93
  """Build a Dockerfile for deployment as a Manual Processing Worker."""
90
94
 
@@ -50,7 +50,7 @@ class Node:
50
50
  def operator(self) -> BashOperator:
51
51
  """Native engine node."""
52
52
  from datetime import timedelta
53
- from dkist_processing_core._failure_callback import chat_ops_notification
53
+ from dkist_processing_core.failure_callback import chat_ops_notification
54
54
  from functools import partial
55
55
 
56
56
  return eval(self.operator_definition)
@@ -69,7 +69,7 @@ class Node:
69
69
  """Airflow style command to define a bash operator."""
70
70
  return f"""BashOperator(
71
71
  task_id='{self.task.__name__}',
72
- bash_command='''{self._bash_script}''',
72
+ bash_command='''{self.bash_script}''',
73
73
  retries={self.task.retries},
74
74
  retry_delay=timedelta(seconds={self.task.retry_delay_seconds}),
75
75
  on_failure_callback=partial(
@@ -89,14 +89,15 @@ class Node:
89
89
  return [(upstream.__name__, self.task.__name__) for upstream in self.upstreams]
90
90
 
91
91
  @property
92
- def _bash_script(self) -> str:
92
+ def bash_script(self) -> str:
93
93
  """Format bash script for the BashOperator."""
94
- command = f"""{self._install_command}
95
- {self._run_command}"""
96
- return self._bash_template(command)
94
+ command = f"""{self.install_command}
95
+ {self.run_command}"""
96
+ return self.bash_template(command)
97
97
 
98
98
  @staticmethod
99
- def _bash_template(command: str) -> str:
99
+ def bash_template(command: str) -> str:
100
+ """Return the bash script with a template wrapped command."""
100
101
  return f"""#!/bin/bash
101
102
  echo Working Directory
102
103
  pwd
@@ -129,26 +130,30 @@ echo Exit with code from main command: $exit_code
129
130
  exit $exit_code"""
130
131
 
131
132
  @property
132
- def _formatted_pip_extras(self) -> str:
133
+ def formatted_pip_extras(self) -> str:
134
+ """Format pip extras for the installation command."""
133
135
  if self.pip_extras:
134
136
  extra_requirements = ",".join(self.pip_extras)
135
137
  return f"'[{extra_requirements}]'"
136
138
  return ""
137
139
 
138
140
  @property
139
- def _install_command(self) -> str:
141
+ def install_command(self) -> str:
142
+ """Format the installation command for the bash script."""
140
143
  repo_name = self.workflow_package.split(".")[0].replace("_", "-")
141
144
  version = self.workflow_version
142
- extras = self._formatted_pip_extras
145
+ extras = self.formatted_pip_extras
143
146
  return f"""python -m pip install --upgrade pip
144
147
  python -m pip install {repo_name}{extras}=={version}"""
145
148
 
146
149
  @property
147
- def _run_command(self) -> str:
148
- return f'python -c "{self._python}"'
150
+ def run_command(self) -> str:
151
+ """Return the python bash command to execute the task."""
152
+ return f'python -c "{self.python}"'
149
153
 
150
154
  @property
151
- def _python(self) -> str:
155
+ def python(self) -> str:
156
+ """Return the python code to execute the task."""
152
157
  return f"""from {self.task.__module__} import {self.task.__name__}
153
158
  with {self.task.__name__}(recipe_run_id={{{{dag_run.conf['recipe_run_id']}}}}, workflow_name='{self.workflow_name}', workflow_version='{self.workflow_version}') as task:
154
159
  task()
@@ -27,17 +27,18 @@ class ApmTransaction:
27
27
  """
28
28
 
29
29
  @property
30
- def _apm_service_name(self) -> str:
30
+ def apm_service_name(self) -> str:
31
+ """Format the service name for Elastic APM."""
31
32
  name = f"{self._workflow_name}-{self._workflow_version}"
32
33
  name = name.replace("_", "-")
33
34
  name = name.replace(".", "-")
34
35
  return name
35
36
 
36
37
  @property
37
- def _apm_config(self) -> dict:
38
+ def apm_config(self) -> dict:
39
+ """Override the Elastic APM configuration with the workflow specific service name."""
38
40
  core_config = core_configurations.apm_config
39
- # Override the service name with the workflow specific name
40
- core_config["SERVICE_NAME"] = self._apm_service_name
41
+ core_config["SERVICE_NAME"] = self.apm_service_name
41
42
  return core_config
42
43
 
43
44
  def __init__(self, transaction_name: str, workflow_name: str, workflow_version: str) -> None:
@@ -46,10 +47,10 @@ class ApmTransaction:
46
47
  self.transaction_name = transaction_name
47
48
 
48
49
  if core_configurations.elastic_apm_enabled:
49
- self.client = elasticapm.Client(self._apm_config)
50
+ self.client = elasticapm.Client(self.apm_config)
50
51
  self.instrument()
51
52
  self.client.begin_transaction(transaction_type="Task")
52
- logger.info(f"APM Configured: {self=} {self._apm_config=}")
53
+ logger.info(f"APM Configured: {self=} {self.apm_config=}")
53
54
  else:
54
55
  logger.warning(f"APM Not Configured")
55
56
 
@@ -11,8 +11,8 @@ from talus import DurableProducer
11
11
  from dkist_processing_core import ResourceQueue
12
12
  from dkist_processing_core import TaskBase
13
13
  from dkist_processing_core import Workflow
14
- from dkist_processing_core._node import Node
15
- from dkist_processing_core._node import task_type_hint
14
+ from dkist_processing_core.node import Node
15
+ from dkist_processing_core.node import task_type_hint
16
16
  from dkist_processing_core.tests.task_example import Task
17
17
 
18
18
 
@@ -2,11 +2,11 @@
2
2
  import pytest
3
3
  from talus import DurableProducer
4
4
 
5
- from dkist_processing_core._failure_callback import chat_ops_notification
6
- from dkist_processing_core._failure_callback import parse_dag_run_id_from_context
7
- from dkist_processing_core._failure_callback import parse_log_url_from_context
8
- from dkist_processing_core._failure_callback import recipe_run_failure_message_producer_factory
9
- from dkist_processing_core._failure_callback import RecipeRunFailureMessage
5
+ from dkist_processing_core.failure_callback import chat_ops_notification
6
+ from dkist_processing_core.failure_callback import parse_dag_run_id_from_context
7
+ from dkist_processing_core.failure_callback import parse_log_url_from_context
8
+ from dkist_processing_core.failure_callback import recipe_run_failure_message_producer_factory
9
+ from dkist_processing_core.failure_callback import RecipeRunFailureMessage
10
10
 
11
11
 
12
12
  @pytest.fixture()
@@ -1,4 +1,4 @@
1
- """Tests of the _node.py module."""
1
+ """Tests of the node.py module."""
2
2
  import subprocess
3
3
  from subprocess import CalledProcessError
4
4
  from typing import Callable
@@ -8,7 +8,7 @@ from airflow.operators.bash import BashOperator
8
8
  from jinja2 import Template
9
9
 
10
10
  from dkist_processing_core import ResourceQueue
11
- from dkist_processing_core._node import Node
11
+ from dkist_processing_core.node import Node
12
12
  from dkist_processing_core.tests.task_example import Task
13
13
 
14
14
 
@@ -38,8 +38,8 @@ def test_nodes(node, fake_producer_factory, queue_name, pip_extras):
38
38
  # passing in just a context dict positional arg with a fake http adapter does not raise an error
39
39
  failure_callback_func({"context": True}, producer_factory=fake_producer_factory)
40
40
  assert isinstance(operator, BashOperator)
41
- assert node._install_command in operator.bash_command
42
- assert node._python in operator.bash_command
41
+ assert node.install_command in operator.bash_command
42
+ assert node.python in operator.bash_command
43
43
  assert node.workflow_name == name
44
44
  assert node.upstreams == upstream
45
45
  assert node.task == task
@@ -57,7 +57,7 @@ def test_node_bash_template_return_0(node):
57
57
  """
58
58
  node, *args = node
59
59
  cmd = 'python -c "pass"'
60
- result = subprocess.run(node._bash_template(cmd), shell=True, check=True)
60
+ result = subprocess.run(node.bash_template(cmd), shell=True, check=True)
61
61
  assert result.returncode == 0
62
62
 
63
63
 
@@ -71,7 +71,7 @@ def test_node_bash_template_return_1(node):
71
71
  node, *args = node
72
72
  cmd = 'python -c "raise Exception"'
73
73
  with pytest.raises(CalledProcessError):
74
- subprocess.run(node._bash_template(cmd), shell=True, check=True)
74
+ subprocess.run(node.bash_template(cmd), shell=True, check=True)
75
75
 
76
76
 
77
77
  def test_node_python(single_node):
@@ -81,7 +81,7 @@ def test_node_python(single_node):
81
81
  Then: no exceptions raised.
82
82
  """
83
83
  # Given
84
- code_template = Template(single_node._python)
84
+ code_template = Template(single_node.python)
85
85
 
86
86
  class RenderData:
87
87
  def __init__(self):
@@ -8,6 +8,7 @@ from airflow import DAG
8
8
  from dkist_processing_core import ResourceQueue
9
9
  from dkist_processing_core import Workflow
10
10
  from dkist_processing_core.workflow import MAXIMUM_ALLOWED_WORKFLOW_NAME_LENGTH
11
+ from dkist_processing_core.workflow import workflow_name_from_details
11
12
 
12
13
 
13
14
  def test_workflow_metadata(workflow):
@@ -39,7 +40,7 @@ def test_workflow_metadata(workflow):
39
40
  assert workflow_instance.input_data == input_data
40
41
  assert workflow_instance.output_data == output_data
41
42
  assert workflow_instance.detail == detail
42
- assert sorted(json.loads(workflow_instance._dag_tags)) == sorted(
43
+ assert sorted(json.loads(workflow_instance.dag_tags)) == sorted(
43
44
  [tag for tag in tags] + [input_data, output_data, category, version]
44
45
  )
45
46
 
@@ -166,3 +167,46 @@ def test_check_dag_name_characters():
166
167
  Workflow.check_dag_name_characters(dag_name="This_dag_name_is_valid")
167
168
  with pytest.raises(ValueError):
168
169
  Workflow.check_dag_name_characters(dag_name="Invalid*dag*name")
170
+
171
+
172
+ @pytest.mark.parametrize(
173
+ "detail",
174
+ [
175
+ pytest.param(None, id="no_detail"),
176
+ pytest.param("detail", id="with_detail"),
177
+ ],
178
+ )
179
+ def test_workflow_name_from_details(detail: str | None):
180
+ """
181
+ Given: a set of details
182
+ When: creating a workflow name
183
+ Then: the workflow name is created correctly
184
+ """
185
+ input_data = "input"
186
+ output_data = "output"
187
+ category = "instrument"
188
+ expected_workflow_name = f"{input_data}_to_{output_data}_{category}"
189
+ if detail:
190
+ expected_workflow_name += f"_{detail}"
191
+ workflow_name = workflow_name_from_details(
192
+ input_data=input_data,
193
+ output_data=output_data,
194
+ category=category,
195
+ detail=detail,
196
+ )
197
+ assert workflow_name == expected_workflow_name
198
+
199
+
200
+ def test_workflow_name_from_details_too_long():
201
+ """
202
+ Given: workflow details with a long input_data value
203
+ When: calling workflow_name_from_details
204
+ Then: a ValueError is raised
205
+ """
206
+
207
+ with pytest.raises(ValueError):
208
+ workflow_name = workflow_name_from_details(
209
+ input_data="".join(["a" for _ in range(MAXIMUM_ALLOWED_WORKFLOW_NAME_LENGTH)]),
210
+ output_data="",
211
+ category="",
212
+ )
@@ -8,16 +8,32 @@ import nbformat as nbf
8
8
  from airflow import DAG
9
9
 
10
10
  from dkist_processing_core import ResourceQueue
11
- from dkist_processing_core._node import Node
12
- from dkist_processing_core._node import task_type_hint
13
- from dkist_processing_core._node import upstreams_type_hint
14
11
  from dkist_processing_core.config import core_configurations
12
+ from dkist_processing_core.node import Node
13
+ from dkist_processing_core.node import task_type_hint
14
+ from dkist_processing_core.node import upstreams_type_hint
15
15
 
16
- __all__ = ["Workflow"]
16
+ __all__ = ["Workflow", "workflow_name_from_details"]
17
17
 
18
18
  MAXIMUM_ALLOWED_WORKFLOW_NAME_LENGTH = 100
19
19
 
20
20
 
21
+ def workflow_name_from_details(
22
+ input_data: str, output_data: str, category: str, detail: str | None = None
23
+ ) -> str:
24
+ """Create the workflow name from its constituent parts."""
25
+ workflow_name = f"{input_data}_to_{output_data}_{category}"
26
+ if detail:
27
+ workflow_name += f"_{detail}"
28
+ workflow_name_too_long = len(workflow_name) > MAXIMUM_ALLOWED_WORKFLOW_NAME_LENGTH
29
+ if workflow_name_too_long:
30
+ raise ValueError(
31
+ f"Workflow name {workflow_name} is {len(workflow_name)} characters long. "
32
+ f"Limit is {MAXIMUM_ALLOWED_WORKFLOW_NAME_LENGTH} characters."
33
+ )
34
+ return workflow_name
35
+
36
+
21
37
  class Workflow:
22
38
  """
23
39
  Abstraction to create a workflow in 1 or more target execution environment.
@@ -84,25 +100,21 @@ class Workflow:
84
100
  if isinstance(tags, str):
85
101
  tags = [tags]
86
102
  self.tags: list[str] = tags or []
87
- self._dag = self._initialize_local_dag()
103
+ self._dag = self.initialize_local_dag()
88
104
  self.nodes = []
89
105
 
90
106
  @property
91
107
  def workflow_name(self) -> str:
92
108
  """Return the workflow name created from its constituent parts."""
93
- result = f"{self.input_data}_to_{self.output_data}_{self.category}"
94
- if self.detail:
95
- result += f"_{self.detail}"
96
- workflow_name_too_long = len(result) > MAXIMUM_ALLOWED_WORKFLOW_NAME_LENGTH
97
- if workflow_name_too_long:
98
- raise ValueError(
99
- f"Workflow name {result} is {len(result)} characters long. "
100
- f"Limit is {MAXIMUM_ALLOWED_WORKFLOW_NAME_LENGTH} characters."
101
- )
102
- return result
109
+ return workflow_name_from_details(
110
+ input_data=self.input_data,
111
+ output_data=self.output_data,
112
+ category=self.category,
113
+ detail=self.detail,
114
+ )
103
115
 
104
116
  @property
105
- def _dag_name(self) -> str:
117
+ def dag_name(self) -> str:
106
118
  """Return the dag name created from its constituent parts."""
107
119
  result = f"{self.workflow_name}_{self.workflow_version}"
108
120
  self.check_dag_name_characters(result) # raise an error if in valid
@@ -133,7 +145,7 @@ class Workflow:
133
145
  )
134
146
 
135
147
  @property
136
- def _dag_tags(self) -> str:
148
+ def dag_tags(self) -> str:
137
149
  """
138
150
  Return the list of dag tags to be used in Airflow's UI.
139
151
 
@@ -146,14 +158,15 @@ class Workflow:
146
158
  return json.dumps(tags)
147
159
 
148
160
  @property
149
- def _dag_definition(self) -> str:
150
-
151
- return f"DAG(dag_id='{self._dag_name}', start_date=pendulum.today('UTC').add(days=-2), schedule=None, catchup=False, tags={self._dag_tags})"
161
+ def dag_definition(self) -> str:
162
+ """Return the string representation of the DAG object instantiation."""
163
+ return f"DAG(dag_id='{self.dag_name}', start_date=pendulum.today('UTC').add(days=-2), schedule=None, catchup=False, tags={self.dag_tags})"
152
164
 
153
- def _initialize_local_dag(self) -> DAG:
165
+ def initialize_local_dag(self) -> DAG:
166
+ """Create a local instance of the DAG object."""
154
167
  import pendulum
155
168
 
156
- return eval(self._dag_definition)
169
+ return eval(self.dag_definition)
157
170
 
158
171
  def add_node(
159
172
  self,
@@ -190,22 +203,22 @@ class Workflow:
190
203
  path = path or "dags/"
191
204
  path = Path(path)
192
205
  path.mkdir(exist_ok=True)
193
- workflow_py = path / f"{self._dag_name}.py"
206
+ workflow_py = path / f"{self.dag_name}.py"
194
207
 
195
208
  with workflow_py.open(mode="w") as f:
196
209
  f.write(
197
210
  f"# {self.workflow_name} workflow version {self.workflow_version} definition rendered for airflow scheduler\n"
198
211
  )
199
- f.write(self._workflow_imports)
212
+ f.write(self.workflow_imports)
200
213
  f.write("# Workflow\n")
201
- f.write(self._workflow_instantiation)
214
+ f.write(self.workflow_instantiation)
202
215
  f.write(" # Nodes\n")
203
216
  for n in self.nodes:
204
217
  operator = f"{n.task.__name__.lower()}_operator"
205
218
  f.write(f" {operator} = {n.operator_definition}")
206
219
  f.write("\n")
207
220
  f.write(" # Edges\n")
208
- f.write(self._workflow_edges)
221
+ f.write(self.workflow_edges)
209
222
  f.write("\n")
210
223
  return workflow_py
211
224
 
@@ -214,7 +227,7 @@ class Workflow:
214
227
  path = path or "notebooks/"
215
228
  path = Path(path)
216
229
  path.mkdir(exist_ok=True)
217
- notebook_ipynb = path / f"{self._dag_name}.ipynb"
230
+ notebook_ipynb = path / f"{self.dag_name}.ipynb"
218
231
 
219
232
  nb = nbf.v4.new_notebook()
220
233
  nb["cells"].append(nbf.v4.new_code_cell("recipe_run_id: int ="))
@@ -233,7 +246,8 @@ class Workflow:
233
246
  return valid_node_order
234
247
 
235
248
  @property
236
- def _workflow_imports(self) -> str:
249
+ def workflow_imports(self) -> str:
250
+ """Return the import statements for the workflow."""
237
251
  imports = [
238
252
  "from datetime import timedelta",
239
253
  "from functools import partial",
@@ -242,18 +256,20 @@ class Workflow:
242
256
  "from airflow.operators.bash import BashOperator",
243
257
  "import pendulum",
244
258
  "",
245
- "from dkist_processing_core._failure_callback import chat_ops_notification",
259
+ "from dkist_processing_core.failure_callback import chat_ops_notification",
246
260
  "",
247
261
  "",
248
262
  ]
249
263
  return "\n".join(imports)
250
264
 
251
265
  @property
252
- def _workflow_instantiation(self) -> str:
253
- return f"with {self._dag_definition} as d:\n pass\n"
266
+ def workflow_instantiation(self) -> str:
267
+ """Return the context manager instantiation of the workflow object."""
268
+ return f"with {self.dag_definition} as d:\n pass\n"
254
269
 
255
270
  @property
256
- def _workflow_edges(self) -> str:
271
+ def workflow_edges(self) -> str:
272
+ """Return the edges between nodes for the workflow."""
257
273
  edges = []
258
274
  for n in self.nodes:
259
275
  for upstream, downstream in n.dependencies:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dkist-processing-core
3
- Version: 4.0.0
3
+ Version: 4.2.0
4
4
  Summary: Abstraction layer that is used by the DKIST Science Data Processing pipelines to process DKIST data using Apache Airflow.
5
5
  Home-page: https://bitbucket.org/dkistdc/dkist-processing-core/src/main/
6
6
  Author: NSO / AURA
@@ -150,12 +150,19 @@ Environment Variables
150
150
 
151
151
  Development
152
152
  -----------
153
+ A prerequisite for test execution is a running instance of rabbitmq and docker on the local machine.
154
+ For RabbitMQ the tests will use the default guest/guest credentials and a host ip of 127.0.0.1 and port of 5672 to connect to the broker.
155
+ Getting docker set up varies by system, but the tests will use the default unix socket for the docker daemon.
156
+
157
+ To run the tests locally, clone the repository and install the package in editable mode with the test extras.
158
+
153
159
  .. code-block:: bash
154
160
 
155
161
  git clone git@bitbucket.org:dkistdc/dkist-processing-core.git
156
162
  cd dkist-processing-core
157
163
  pre-commit install
158
164
  pip install -e .[test]
165
+ # RabbitMQ and Docker needs to be running
159
166
  pytest -v --cov dkist_processing_core
160
167
 
161
168
  Changelog
@@ -10,10 +10,10 @@ setup.cfg
10
10
  setup.py
11
11
  changelog/.gitempty
12
12
  dkist_processing_core/__init__.py
13
- dkist_processing_core/_failure_callback.py
14
- dkist_processing_core/_node.py
15
13
  dkist_processing_core/build_utils.py
16
14
  dkist_processing_core/config.py
15
+ dkist_processing_core/failure_callback.py
16
+ dkist_processing_core/node.py
17
17
  dkist_processing_core/resource_queue.py
18
18
  dkist_processing_core/task.py
19
19
  dkist_processing_core/workflow.py
@@ -1,4 +1,4 @@
1
- apache-airflow[celery,postgres]==2.9.1
1
+ apache-airflow[celery,postgres]==2.10.2
2
2
  elastic-apm<7.0.0
3
3
  requests>=2.23
4
4
  talus<2.0,>=1.1.0
@@ -25,3 +25,4 @@ pytest-mock
25
25
  jinja2
26
26
  towncrier
27
27
  nbconvert
28
+ ipython
@@ -4,6 +4,7 @@
4
4
  directory = "changelog/"
5
5
  issue_format = "`#{issue} <https://bitbucket.org/dkistdc/dkist-processing-core/pull-requests/{issue}>`__"
6
6
  title_format = "{version} ({project_date})"
7
+ ignore = [".gitempty"]
7
8
 
8
9
  [[tool.towncrier.type]]
9
10
  directory = "feature"
@@ -19,7 +19,7 @@ setup_requires = setuptools_scm
19
19
  packages = find:
20
20
  include_package_data = True
21
21
  install_requires =
22
- apache-airflow[postgres, celery] == 2.9.1
22
+ apache-airflow[postgres, celery] == 2.10.2
23
23
  elastic-apm < 7.0.0
24
24
  requests >= 2.23
25
25
  talus >= 1.1.0, <2.0
@@ -36,6 +36,7 @@ test =
36
36
  jinja2
37
37
  towncrier
38
38
  nbconvert
39
+ ipython
39
40
  docs =
40
41
  sphinx
41
42
  sphinx-astropy