From 966a640dcbd4f71d7581e7b9387f2ff5318a4fb8 Mon Sep 17 00:00:00 2001 From: fritz-astronomer <80706212+fritz-astronomer@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:28:38 -0400 Subject: [PATCH] Deployed 131c3f5 with MkDocs version: 1.6.0 --- 404.html | 4 + Rules_and_Rulesets/index.html | 530 +++++++++--------- Rules_and_Rulesets/rules/index.html | 24 +- Rules_and_Rulesets/rulesets/index.html | 330 +++++------ Rules_and_Rulesets/template/index.html | 4 + assets/_markdown_exec_pyodide.css | 50 ++ assets/_markdown_exec_pyodide.js | 109 ++++ cli/index.html | 216 ++++++- index.html | 23 +- .../callbacks/index.html | 4 + .../operators/index.html | 14 + objects/Tasks/index.html | 8 +- objects/dags/index.html | 19 +- objects/index.html | 10 +- objects/project/index.html | 23 +- orbiter_diagram.png | Bin 362653 -> 309139 bytes origins/index.html | 136 +++-- search/search_index.json | 2 +- sitemap.xml | 26 +- sitemap.xml.gz | Bin 314 -> 314 bytes 20 files changed, 970 insertions(+), 562 deletions(-) create mode 100644 assets/_markdown_exec_pyodide.css create mode 100644 assets/_markdown_exec_pyodide.js diff --git a/404.html b/404.html index 6c0e618..d13be95 100644 --- a/404.html +++ b/404.html @@ -42,6 +42,8 @@ + + @@ -754,6 +756,8 @@

404 - Not found

+ + \ No newline at end of file diff --git a/Rules_and_Rulesets/index.html b/Rules_and_Rulesets/index.html index 2d7fe66..c931c8e 100644 --- a/Rules_and_Rulesets/index.html +++ b/Rules_and_Rulesets/index.html @@ -48,6 +48,8 @@ + + @@ -805,29 +807,26 @@

Overview -

The "brain" of the Orbiter framework is in it's Rules +

The brain of the Orbiter framework is in it's Rules and the Rulesets that contain them.

-

Diagram of Orbiter Translation

Different Rules are applied in different scenarios; such as for converting input to a DAG (@dag_rule), or a specific Airflow Operator (@task_rule), or for filtering entries from the input data (@dag_filter_rule, @task_filter_rule).

-

A Rule should evaluate to a single something or nothing.

Tip

-

If we want to map the following input +

To map the following input

{
     "id": "my_task",
     "command": "echo 'hi'"
@@ -882,30 +881,29 @@ 

-

Orbiter, by default, expects a folder containing text files (.json, .xml, .yaml, etc.) -which may have a structure like: +

Orbiter expects a folder containing text files which may have a structure like:

{"<workflow name>": { ...<workflow properties>, "<task name>": { ...<task properties>} }}
 

The default translation function (orbiter.rules.rulesets.translate) performs the following steps:

+

Diagram of Orbiter Translation

    -
  1. Look in the input folder for all files with the expected -TranslationRuleset.file_type. - For each file, it will:
      -
    1. Load the file and turn it into a Python Dictionary
    2. -
    3. Apply the TranslationRuleset.dag_filter_ruleset - to filter down to keys suspected of being translatable to a DAG, - in priority order. For each suspected DAG dict:
        -
      1. Apply the TranslationRuleset.dag_ruleset, - to convert the object to an OrbiterDAG, - in priority-order, stopping when the first rule returns a match.
      2. +
      3. Find all files with the expected + TranslationRuleset.file_type + (.json, .xml, .yaml, etc.) in the input folder. Load each file and turn it into a Python Dictionary.
      4. +
      5. For each file: Apply the TranslationRuleset.dag_filter_ruleset + to filter down to entries that can translate to a DAG, in priority order.
          +
        • For each: Apply the TranslationRuleset.dag_ruleset, +to convert the object to an OrbiterDAG, +in priority-order, stopping when the first rule returns a match. +If no rule returns a match, the entry is filtered.
        • +
        +
      6. Apply the TranslationRuleset.task_filter_ruleset - to filter down to keys suspected of being translatable to a Task, - in priority-order. For each suspected Task dict:
          -
        1. Apply the TranslationRuleset.task_ruleset, - in priority-order, stopping when the first rule returns a match, - to convert the dictionary to a specific type of Task. If no rule returns a match, - the dict is filtered.
        2. -
        + to filter down to entries in the DAG that can translate to a Task, in priority-order.
          +
        • For each: Apply the TranslationRuleset.task_ruleset, + to convert the object to a specific Task, in priority-order, stopping when the first rule returns a match. + If no rule returns a match, the entry is filtered.
        • +
      7. After the DAG and Tasks are mapped, the TranslationRuleset.task_dependency_ruleset @@ -914,16 +912,12 @@

        OrbiterTaskDependency, which are then added to each task in the OrbiterDAG

      8. -
      -
    4. -
    -
  2. Apply the TranslationRuleset.post_processing_ruleset, against the OrbiterProject, which can make modifications after all other rules have been applied.
  3. -
  4. Return the OrbiterProject
  5. +
  6. After translation - the OrbiterProject + is rendered to the output folder.
-

After translation - the OrbiterProject is rendered to the output folder.

Source code in orbiter/rules/rulesets.py @@ -1071,153 +1065,151 @@

240 241 242 -243 -244

@validate_call
+243
@validate_call
 def translate(translation_ruleset, input_dir: Path) -> OrbiterProject:
     """
-    Orbiter, by default, expects a folder containing text files (`.json`, `.xml`, `.yaml`, etc.)
-    which may have a structure like:
-    ```json
-    {"<workflow name>": { ...<workflow properties>, "<task name>": { ...<task properties>} }}
-    ```
-
-    The default translation function (`orbiter.rules.rulesets.translate`) performs the following steps:
-
-    1. Look in the input folder for all files with the expected
-    [`TranslationRuleset.file_type`][orbiter.rules.rulesets.TranslationRuleset].
-        For each file, it will:
-        1. Load the file and turn it into a Python Dictionary
-        2. Apply the [`TranslationRuleset.dag_filter_ruleset`][orbiter.rules.rulesets.DAGFilterRuleset]
-            to filter down to keys suspected of being translatable to a DAG,
-            in priority order. For each suspected DAG dict:
-            1. Apply the [`TranslationRuleset.dag_ruleset`][orbiter.rules.rulesets.DAGRuleset],
-                to convert the object to an [`OrbiterDAG`][orbiter.objects.dag.OrbiterDAG],
-                in priority-order, stopping when the first rule returns a match.
-            2. Apply the [`TranslationRuleset.task_filter_ruleset`][orbiter.rules.rulesets.TaskFilterRuleset]
-                to filter down to keys suspected of being translatable to a Task,
-                in priority-order. For each suspected Task dict:
-                1. Apply the [`TranslationRuleset.task_ruleset`][orbiter.rules.rulesets.TaskRuleset],
-                    in priority-order, stopping when the first rule returns a match,
-                    to convert the dictionary to a specific type of Task. If no rule returns a match,
-                    the dict is filtered.
-            3. After the DAG and Tasks are mapped, the
-                [`TranslationRuleset.task_dependency_ruleset`][orbiter.rules.rulesets.TaskDependencyRuleset]
-                is applied in priority-order, stopping when the first rule returns a match,
-                to create a list of
-                [`OrbiterTaskDependency`][orbiter.objects.task.OrbiterTaskDependency],
-                which are then added to each task in the
-                [`OrbiterDAG`][orbiter.objects.dag.OrbiterDAG]
-    2. Apply the [`TranslationRuleset.post_processing_ruleset`][orbiter.rules.rulesets.PostProcessingRuleset],
-        against the [`OrbiterProject`][orbiter.objects.project.OrbiterProject], which can make modifications after all
-        other rules have been applied.
-    3. Return the [`OrbiterProject`][orbiter.objects.project.OrbiterProject]
+    Orbiter expects a folder containing text files which may have a structure like:
+    ```json
+    {"<workflow name>": { ...<workflow properties>, "<task name>": { ...<task properties>} }}
+    ```
+
+    The default translation function (`orbiter.rules.rulesets.translate`) performs the following steps:
+
+    ![Diagram of Orbiter Translation](../orbiter_diagram.png)
+
+    1. **Find all files** with the expected
+        [`TranslationRuleset.file_type`][orbiter.rules.rulesets.TranslationRuleset]
+        (`.json`, `.xml`, `.yaml`, etc.) in the input folder. Load each file and turn it into a Python Dictionary.
+    2. **For each file:** Apply the [`TranslationRuleset.dag_filter_ruleset`][orbiter.rules.rulesets.DAGFilterRuleset]
+        to filter down to entries that can translate to a DAG, in priority order.
+        - **For each**: Apply the [`TranslationRuleset.dag_ruleset`][orbiter.rules.rulesets.DAGRuleset],
+        to convert the object to an [`OrbiterDAG`][orbiter.objects.dag.OrbiterDAG],
+        in priority-order, stopping when the first rule returns a match.
+        If no rule returns a match, the entry is filtered.
+    3. Apply the [`TranslationRuleset.task_filter_ruleset`][orbiter.rules.rulesets.TaskFilterRuleset]
+        to filter down to entries in the DAG that can translate to a Task, in priority-order.
+        - **For each:** Apply the [`TranslationRuleset.task_ruleset`][orbiter.rules.rulesets.TaskRuleset],
+            to convert the object to a specific Task, in priority-order, stopping when the first rule returns a match.
+            If no rule returns a match, the entry is filtered.
+    4. After the DAG and Tasks are mapped, the
+        [`TranslationRuleset.task_dependency_ruleset`][orbiter.rules.rulesets.TaskDependencyRuleset]
+        is applied in priority-order, stopping when the first rule returns a match,
+        to create a list of
+        [`OrbiterTaskDependency`][orbiter.objects.task.OrbiterTaskDependency],
+        which are then added to each task in the
+        [`OrbiterDAG`][orbiter.objects.dag.OrbiterDAG]
+    5. Apply the [`TranslationRuleset.post_processing_ruleset`][orbiter.rules.rulesets.PostProcessingRuleset],
+        against the [`OrbiterProject`][orbiter.objects.project.OrbiterProject], which can make modifications after all
+        other rules have been applied.
+    6. After translation - the [`OrbiterProject`][orbiter.objects.project.OrbiterProject]
+        is rendered to the output folder.
+
 
-    After translation - the [`OrbiterProject`][orbiter.objects.project.OrbiterProject] is rendered to the output folder.
-    """
-
-    def _get_files_with_extension(_extension: str, _input_dir: Path) -> List[Path]:
-        return [
-            directory / file
-            for (directory, _, files) in _input_dir.walk()
-            for file in files
-            if _extension == file.lower()[-len(_extension) :]
-        ]
-
-    if not isinstance(translation_ruleset, TranslationRuleset):
-        raise RuntimeError(
-            f"Error! type(translation_ruleset)=={type(translation_ruleset)}!=TranslationRuleset! Exiting!"
-        )
-
-    # Create an initial OrbiterProject
-    project = OrbiterProject()
-
-    extension = translation_ruleset.file_type.value.lower()
-
-    logger.info(f"Finding files with extension={extension} in {input_dir}")
-    files = _get_files_with_extension(extension, input_dir)
-
-    # .yaml is sometimes '.yml'
-    if extension == "yaml":
-        files.extend(_get_files_with_extension("yml", input_dir))
-
-    logger.info(f"Found {len(files)} files with extension={extension} in {input_dir}")
-
-    for file in files:
-        logger.info(f"Translating file={file.resolve()}")
-
-        # Load the file and convert it into a python dict
-        input_dict = load_filetype(file.read_text(), translation_ruleset.file_type)
-
-        # DAG FILTER Ruleset - filter down to keys suspected of being translatable to a DAG, in priority order.
-        dag_dicts = functools.reduce(
-            add,
-            translation_ruleset.dag_filter_ruleset.apply(val=input_dict),
-            [],
-        )
-        logger.debug(f"Found {len(dag_dicts)} DAG candidates in {file.resolve()}")
-        for dag_dict in dag_dicts:
-            # DAG Ruleset - convert the object to an `OrbiterDAG` via `dag_ruleset`,
-            #         in priority-order, stopping when the first rule returns a match
-            dag: OrbiterDAG | None = translation_ruleset.dag_ruleset.apply(
-                val=dag_dict,
-                take_first=True,
-            )
-            if dag is None:
-                logger.warning(
-                    f"Couldn't extract DAG from dag_dict={dag_dict} with dag_ruleset={translation_ruleset.dag_ruleset}"
-                )
-                continue
-            dag.orbiter_kwargs["file_path"] = str(file.resolve())
-
-            tasks = {}
-            # TASK FILTER Ruleset - Many entries in dag_dict -> Many task_dict
-            task_dicts = functools.reduce(
-                add,
-                translation_ruleset.task_filter_ruleset.apply(val=dag_dict),
-                [],
-            )
-            logger.debug(
-                f"Found {len(task_dicts)} Task candidates in {dag.dag_id} in {file.resolve()}"
-            )
-            for task_dict in task_dicts:
-                # TASK Ruleset one -> one
-                task: OrbiterOperator = translation_ruleset.task_ruleset.apply(
-                    val=task_dict, take_first=True
-                )
-                if task is None:
-                    logger.warning(
-                        f"Couldn't extract task from expected task_dict={task_dict}"
-                    )
-                    continue
-
-                _add_task_deduped(task, tasks)
-            logger.debug(f"Adding {len(tasks)} tasks to DAG {dag.dag_id}")
-            dag.add_tasks(tasks.values())
-
-            # Dag-Level TASK DEPENDENCY Ruleset
-            task_dependencies: List[OrbiterTaskDependency] = (
-                list(chain(*translation_ruleset.task_dependency_ruleset.apply(val=dag)))
-                or []
-            )
-            if not len(task_dependencies):
-                logger.warning(f"Couldn't find task dependencies in dag={dag_dict}")
-            for task_dependency in task_dependencies:
-                task_dependency: OrbiterTaskDependency
-                if task_dependency.task_id not in dag.tasks:
-                    logger.warning(
-                        f"Couldn't find task_id={task_dependency.task_id} in tasks={tasks} for dag_id={dag.dag_id}"
-                    )
-                    continue
-                else:
-                    dag.tasks[task_dependency.task_id].add_downstream(task_dependency)
-
-            logger.debug(f"Adding DAG {dag.dag_id} to project")
-            project.add_dags(dag)
-
-    # POST PROCESSING Ruleset
-    translation_ruleset.post_processing_ruleset.apply(val=project, take_first=False)
-
-    return project
+    """
+
+    def _get_files_with_extension(_extension: str, _input_dir: Path) -> List[Path]:
+        return [
+            directory / file
+            for (directory, _, files) in _input_dir.walk()
+            for file in files
+            if _extension == file.lower()[-len(_extension) :]
+        ]
+
+    if not isinstance(translation_ruleset, TranslationRuleset):
+        raise RuntimeError(
+            f"Error! type(translation_ruleset)=={type(translation_ruleset)}!=TranslationRuleset! Exiting!"
+        )
+
+    # Create an initial OrbiterProject
+    project = OrbiterProject()
+
+    extension = translation_ruleset.file_type.value.lower()
+
+    logger.info(f"Finding files with extension={extension} in {input_dir}")
+    files = _get_files_with_extension(extension, input_dir)
+
+    # .yaml is sometimes '.yml'
+    if extension == "yaml":
+        files.extend(_get_files_with_extension("yml", input_dir))
+
+    logger.info(f"Found {len(files)} files with extension={extension} in {input_dir}")
+
+    for file in files:
+        logger.info(f"Translating file={file.resolve()}")
+
+        # Load the file and convert it into a python dict
+        input_dict = load_filetype(file.read_text(), translation_ruleset.file_type)
+
+        # DAG FILTER Ruleset - filter down to keys suspected of being translatable to a DAG, in priority order.
+        dag_dicts = functools.reduce(
+            add,
+            translation_ruleset.dag_filter_ruleset.apply(val=input_dict),
+            [],
+        )
+        logger.debug(f"Found {len(dag_dicts)} DAG candidates in {file.resolve()}")
+        for dag_dict in dag_dicts:
+            # DAG Ruleset - convert the object to an `OrbiterDAG` via `dag_ruleset`,
+            #         in priority-order, stopping when the first rule returns a match
+            dag: OrbiterDAG | None = translation_ruleset.dag_ruleset.apply(
+                val=dag_dict,
+                take_first=True,
+            )
+            if dag is None:
+                logger.warning(
+                    f"Couldn't extract DAG from dag_dict={dag_dict} with dag_ruleset={translation_ruleset.dag_ruleset}"
+                )
+                continue
+            dag.orbiter_kwargs["file_path"] = str(file.resolve())
+
+            tasks = {}
+            # TASK FILTER Ruleset - Many entries in dag_dict -> Many task_dict
+            task_dicts = functools.reduce(
+                add,
+                translation_ruleset.task_filter_ruleset.apply(val=dag_dict),
+                [],
+            )
+            logger.debug(
+                f"Found {len(task_dicts)} Task candidates in {dag.dag_id} in {file.resolve()}"
+            )
+            for task_dict in task_dicts:
+                # TASK Ruleset one -> one
+                task: OrbiterOperator = translation_ruleset.task_ruleset.apply(
+                    val=task_dict, take_first=True
+                )
+                if task is None:
+                    logger.warning(
+                        f"Couldn't extract task from expected task_dict={task_dict}"
+                    )
+                    continue
+
+                _add_task_deduped(task, tasks)
+            logger.debug(f"Adding {len(tasks)} tasks to DAG {dag.dag_id}")
+            dag.add_tasks(tasks.values())
+
+            # Dag-Level TASK DEPENDENCY Ruleset
+            task_dependencies: List[OrbiterTaskDependency] = (
+                list(chain(*translation_ruleset.task_dependency_ruleset.apply(val=dag)))
+                or []
+            )
+            if not len(task_dependencies):
+                logger.warning(f"Couldn't find task dependencies in dag={dag_dict}")
+            for task_dependency in task_dependencies:
+                task_dependency: OrbiterTaskDependency
+                if task_dependency.task_id not in dag.tasks:
+                    logger.warning(
+                        f"Couldn't find task_id={task_dependency.task_id} in tasks={tasks} for dag_id={dag.dag_id}"
+                    )
+                    continue
+                else:
+                    dag.tasks[task_dependency.task_id].add_downstream(task_dependency)
+
+            logger.debug(f"Adding DAG {dag.dag_id} to project")
+            project.add_dags(dag)
+
+    # POST PROCESSING Ruleset
+    translation_ruleset.post_processing_ruleset.apply(val=project, take_first=False)
+
+    return project
 
@@ -1264,7 +1256,8 @@

Source code in orbiter/rules/rulesets.py -
615
+              
614
+615
 616
 617
 618
@@ -1287,32 +1280,31 @@ 

635 636 637 -638 -639

@validate_call
-def load_filetype(input_str: str, file_type: FileType) -> dict:
-    """
-    Orbiter converts all file types into a Python dictionary "intermediate representation" form,
-    prior to any rulesets being applied.
-
-    | FileType | Conversion Method                                           |
-    |----------|-------------------------------------------------------------|
-    | `XML`    | [`xmltodict_parse`][orbiter.rules.rulesets.xmltodict_parse] |
-    | `YAML`   | `yaml.safe_load`                                            |
-    | `JSON`   | `json.loads`                                                |
-    """
-
-    if file_type == FileType.JSON:
-        import json
-
-        return json.loads(input_str)
-    elif file_type == FileType.YAML:
-        import yaml
-
-        return yaml.safe_load(input_str)
-    elif file_type == FileType.XML:
-        return xmltodict_parse(input_str)
-    else:
-        raise NotImplementedError(f"Cannot load file_type={file_type}")
+638
@validate_call
+def load_filetype(input_str: str, file_type: FileType) -> dict:
+    """
+    Orbiter converts all file types into a Python dictionary "intermediate representation" form,
+    prior to any rulesets being applied.
+
+    | FileType | Conversion Method                                           |
+    |----------|-------------------------------------------------------------|
+    | `XML`    | [`xmltodict_parse`][orbiter.rules.rulesets.xmltodict_parse] |
+    | `YAML`   | `yaml.safe_load`                                            |
+    | `JSON`   | `json.loads`                                                |
+    """
+
+    if file_type == FileType.JSON:
+        import json
+
+        return json.loads(input_str)
+    elif file_type == FileType.YAML:
+        import yaml
+
+        return yaml.safe_load(input_str)
+    elif file_type == FileType.XML:
+        return xmltodict_parse(input_str)
+    else:
+        raise NotImplementedError(f"Cannot load file_type={file_type}")
 
@@ -1410,7 +1402,8 @@

Source code in orbiter/rules/rulesets.py -
643
+              
642
+643
 644
 645
 646
@@ -1470,69 +1463,68 @@ 

700 701 702 -703 -704

def xmltodict_parse(input_str: str) -> Any:
-    """Calls `xmltodict.parse` and does post-processing fixes.
-
-    !!! note
-
-        The original [`xmltodict.parse`](https://pypi.org/project/xmltodict/) method returns EITHER:
-
-        - a dict (one child element of type)
-        - or a list of dict (many child element of type)
-
-        This behavior can be confusing, and is an issue with the original xml spec being referenced.
-
-        **This method deviates by standardizing to the latter case (always a `list[dict]`).**
-
-        **All XML elements will be a list of dictionaries, even if there's only one element.**
-
-    ```pycon
-    >>> xmltodict_parse("")
-    Traceback (most recent call last):
-    xml.parsers.expat.ExpatError: no element found: line 1, column 0
-    >>> xmltodict_parse("<a></a>")
-    {'a': None}
-    >>> xmltodict_parse("<a foo='bar'></a>")
-    {'a': [{'@foo': 'bar'}]}
-    >>> xmltodict_parse("<a foo='bar'><foo bar='baz'></foo></a>")  # Singleton - gets modified
-    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}]}]}
-    >>> xmltodict_parse("<a foo='bar'><foo bar='baz'><bar><bop></bop></bar></foo></a>")  # Nested Singletons - modified
-    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz', 'bar': [{'bop': None}]}]}]}
-    >>> xmltodict_parse("<a foo='bar'><foo bar='baz'></foo><foo bing='bop'></foo></a>")
-    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}, {'@bing': 'bop'}]}]}
-
-    ```
-    :param input_str: The XML string to parse
-    :type input_str: str
-    :return: The parsed XML
-    :rtype: dict
-    """
-    import xmltodict
-
-    # noinspection t
-    def _fix(d):
-        """fix the dict in place, recursively, standardizing on a list of dict even if there's only one entry."""
-        # if it's a dict, descend to fix
-        if isinstance(d, dict):
-            for k, v in d.items():
-                # @keys are properties of elements, non-@keys are elements
-                if not k.startswith("@"):
-                    if isinstance(v, dict):
-                        # THE FIX
-                        # any non-@keys should be a list of dict, even if there's just one of the element
-                        d[k] = [v]
-                        _fix(v)
-                    else:
-                        _fix(v)
-        # if it's a list, descend to fix
-        if isinstance(d, list):
-            for v in d:
-                _fix(v)
-
-    output = xmltodict.parse(input_str)
-    _fix(output)
-    return output
+703
def xmltodict_parse(input_str: str) -> Any:
+    """Calls `xmltodict.parse` and does post-processing fixes.
+
+    !!! note
+
+        The original [`xmltodict.parse`](https://pypi.org/project/xmltodict/) method returns EITHER:
+
+        - a dict (one child element of type)
+        - or a list of dict (many child element of type)
+
+        This behavior can be confusing, and is an issue with the original xml spec being referenced.
+
+        **This method deviates by standardizing to the latter case (always a `list[dict]`).**
+
+        **All XML elements will be a list of dictionaries, even if there's only one element.**
+
+    ```pycon
+    >>> xmltodict_parse("")
+    Traceback (most recent call last):
+    xml.parsers.expat.ExpatError: no element found: line 1, column 0
+    >>> xmltodict_parse("<a></a>")
+    {'a': None}
+    >>> xmltodict_parse("<a foo='bar'></a>")
+    {'a': [{'@foo': 'bar'}]}
+    >>> xmltodict_parse("<a foo='bar'><foo bar='baz'></foo></a>")  # Singleton - gets modified
+    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}]}]}
+    >>> xmltodict_parse("<a foo='bar'><foo bar='baz'><bar><bop></bop></bar></foo></a>")  # Nested Singletons - modified
+    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz', 'bar': [{'bop': None}]}]}]}
+    >>> xmltodict_parse("<a foo='bar'><foo bar='baz'></foo><foo bing='bop'></foo></a>")
+    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}, {'@bing': 'bop'}]}]}
+
+    ```
+    :param input_str: The XML string to parse
+    :type input_str: str
+    :return: The parsed XML
+    :rtype: dict
+    """
+    import xmltodict
+
+    # noinspection t
+    def _fix(d):
+        """fix the dict in place, recursively, standardizing on a list of dict even if there's only one entry."""
+        # if it's a dict, descend to fix
+        if isinstance(d, dict):
+            for k, v in d.items():
+                # @keys are properties of elements, non-@keys are elements
+                if not k.startswith("@"):
+                    if isinstance(v, dict):
+                        # THE FIX
+                        # any non-@keys should be a list of dict, even if there's just one of the element
+                        d[k] = [v]
+                        _fix(v)
+                    else:
+                        _fix(v)
+        # if it's a list, descend to fix
+        if isinstance(d, list):
+            for v in d:
+                _fix(v)
+
+    output = xmltodict.parse(input_str)
+    _fix(output)
+    return output
 
@@ -1597,6 +1589,8 @@

+ + \ No newline at end of file diff --git a/Rules_and_Rulesets/rules/index.html b/Rules_and_Rulesets/rules/index.html index 96e1a1a..74139ec 100644 --- a/Rules_and_Rulesets/rules/index.html +++ b/Rules_and_Rulesets/rules/index.html @@ -48,6 +48,8 @@ + + @@ -850,7 +852,7 @@

A Rule contains a python function that is evaluated and produces something -(typically an Orbiter Object) or nothing

+(typically an Object) or nothing

A Rule can be created from a decorator

>>> @rule(priority=1)
 ... def my_rule(val):
@@ -1011,8 +1013,11 @@ 

A @dag_rule decorator creates a DAGRule

@dag_rule
-def foo(val: dict) -> List[dict]:
-    return OrbiterDAG(dag_id="foo")
+def foo(val: dict) -> OrbiterDAG | None:
+    if 'id' in val:
+        return OrbiterDAG(dag_id=val["id"], file_path=f"{val["id"]}.py")
+    else:
+        return None
 
@@ -1054,7 +1059,7 @@

A @task_filter_rule decorator creates a TaskFilterRule

@task_filter_rule
-def foo(val: dict) -> List[dict]:
+def foo(val: dict) -> List[dict] | None:
     return [{"task_id": "foo"}]
 
@@ -1151,7 +1156,10 @@

A @task_rule decorator creates a TaskRule

@task_rule
 def foo(val: dict) -> OrbiterOperator | OrbiterTaskGroup:
-    return OrbiterOperator(task_id="foo")
+    if 'id' in val and 'command' in val:
+        return OrbiterBashOperator(task_id=val['id'], bash_command=val['command'])
+    else:
+        return None
 
@@ -1245,7 +1253,7 @@

and returns a list[OrbiterTaskDependency] or None

@task_dependency_rule
 def foo(val: OrbiterDAG) -> OrbiterTaskDependency:
-    return [OrbiterTaskDependency(task_id="task_id", downstream="downstream")]
+    return [OrbiterTaskDependency(task_id="upstream", downstream="downstream")]
 
@@ -1339,7 +1347,7 @@

after all other rules have been applied, and modifies it in-place.

@post_processing_rule
 def foo(val: OrbiterProject) -> None:
-    val.dags["foo"].tasks["bar"].description = "Hello World"
+    val.dags["foo"].tasks["bar"].doc = "Hello World"
 
@@ -1470,6 +1478,8 @@

+ + \ No newline at end of file diff --git a/Rules_and_Rulesets/rulesets/index.html b/Rules_and_Rulesets/rulesets/index.html index 52a562c..f211de3 100644 --- a/Rules_and_Rulesets/rulesets/index.html +++ b/Rules_and_Rulesets/rulesets/index.html @@ -48,6 +48,8 @@ + + @@ -1245,7 +1247,8 @@

Source code in orbiter/rules/rulesets.py -
366
+              
365
+366
 367
 368
 369
@@ -1341,105 +1344,104 @@ 

459 460 461 -462 -463

@validate_call
-def apply(self, take_first: bool = False, **kwargs) -> List[Any] | Any:
-    """
-    Apply all rules in ruleset **to a single item**, in priority order, removing any `None` results.
-
-    A ruleset with one rule can produce **up to one** result
-    ```pycon
-    >>> from orbiter.rules import rule
-
-    >>> @rule
-    ... def gt_4(val):
-    ...     return str(val) if val > 4 else None
-    >>> Ruleset(ruleset=[gt_4]).apply(val=5)
-    ['5']
-
-    ```
-
-    Many rules can produce many results, one for each rule.
-    ```pycon
-    >>> @rule
-    ... def gt_3(val):
-    ...    return str(val) if val > 3 else None
-    >>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5)
-    ['5', '5']
-
-    ```
-
-    The `take_first` flag will evaluate rules in the ruleset and return the first match
-    ```pycon
-    >>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5, take_first=True)
-    '5'
-
-    ```
-
-    If nothing matched, an empty list is returned
-    ```pycon
-    >>> @rule
-    ... def always_none(val):
-    ...     return None
-    >>> @rule
-    ... def more_always_none(val):
-    ...     return None
-    >>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5)
-    []
-
-    ```
-
-    If nothing matched, and `take_first=True`, `None` is returned
-    ```pycon
-    >>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5, take_first=True)
-    ... # None
-
-    ```
-
-    !!! tip
-
-        If no input is given, an error is returned
-        ```pycon
-        >>> Ruleset(ruleset=[always_none]).apply()
-        Traceback (most recent call last):
-        RuntimeError: No values provided! Supply at least one key=val pair as kwargs!
-
-        ```
-
-    :param take_first: only take the first (if any) result from the ruleset application
-    :type take_first: bool
-    :param kwargs: key=val pairs to pass to the evaluated rule function
-    :returns: List of rules that evaluated to `Any` (in priority order),
-                or an empty list,
-                or `Any` (if `take_first=True`)
-    :rtype: List[Any] | Any | None
-    :raises RuntimeError: if the Ruleset is empty or input_val is None
-    :raises RuntimeError: if the Rule raises an exception
-    """
-    if not len(kwargs):
-        raise RuntimeError(
-            "No values provided! Supply at least one key=val pair as kwargs!"
-        )
-    results = []
-    for _rule in self._sorted():
-        result = _rule(**kwargs)
-        should_show_input = "val" in kwargs and not (
-            isinstance(kwargs["val"], OrbiterProject)
-            or isinstance(kwargs["val"], OrbiterDAG)
-        )
-        if result is not None:
-            logger.debug(
-                "---------\n"
-                f"[RULESET MATCHED] '{self.__class__.__module__}.{self.__class__.__name__}'\n"
-                f"[RULE MATCHED] '{_rule.__name__}'\n"
-                f"[INPUT] {kwargs if should_show_input else '<Skipping...>'}\n"
-                f"[RETURN] {result}\n"
-                f"---------"
-            )
-            results.append(result)
-            if take_first:
-                return result
-    return None if take_first and not len(results) else results
+462
@validate_call
+def apply(self, take_first: bool = False, **kwargs) -> List[Any] | Any:
+    """
+    Apply all rules in ruleset **to a single item**, in priority order, removing any `None` results.
+
+    A ruleset with one rule can produce **up to one** result
+    ```pycon
+    >>> from orbiter.rules import rule
+
+    >>> @rule
+    ... def gt_4(val):
+    ...     return str(val) if val > 4 else None
+    >>> Ruleset(ruleset=[gt_4]).apply(val=5)
+    ['5']
+
+    ```
+
+    Many rules can produce many results, one for each rule.
+    ```pycon
+    >>> @rule
+    ... def gt_3(val):
+    ...    return str(val) if val > 3 else None
+    >>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5)
+    ['5', '5']
+
+    ```
+
+    The `take_first` flag will evaluate rules in the ruleset and return the first match
+    ```pycon
+    >>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5, take_first=True)
+    '5'
+
+    ```
+
+    If nothing matched, an empty list is returned
+    ```pycon
+    >>> @rule
+    ... def always_none(val):
+    ...     return None
+    >>> @rule
+    ... def more_always_none(val):
+    ...     return None
+    >>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5)
+    []
+
+    ```
+
+    If nothing matched, and `take_first=True`, `None` is returned
+    ```pycon
+    >>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5, take_first=True)
+    ... # None
+
+    ```
+
+    !!! tip
+
+        If no input is given, an error is returned
+        ```pycon
+        >>> Ruleset(ruleset=[always_none]).apply()
+        Traceback (most recent call last):
+        RuntimeError: No values provided! Supply at least one key=val pair as kwargs!
+
+        ```
+
+    :param take_first: only take the first (if any) result from the ruleset application
+    :type take_first: bool
+    :param kwargs: key=val pairs to pass to the evaluated rule function
+    :returns: List of rules that evaluated to `Any` (in priority order),
+                or an empty list,
+                or `Any` (if `take_first=True`)
+    :rtype: List[Any] | Any | None
+    :raises RuntimeError: if the Ruleset is empty or input_val is None
+    :raises RuntimeError: if the Rule raises an exception
+    """
+    if not len(kwargs):
+        raise RuntimeError(
+            "No values provided! Supply at least one key=val pair as kwargs!"
+        )
+    results = []
+    for _rule in self._sorted():
+        result = _rule(**kwargs)
+        should_show_input = "val" in kwargs and not (
+            isinstance(kwargs["val"], OrbiterProject)
+            or isinstance(kwargs["val"], OrbiterDAG)
+        )
+        if result is not None:
+            logger.debug(
+                "---------\n"
+                f"[RULESET MATCHED] '{self.__class__.__module__}.{self.__class__.__name__}'\n"
+                f"[RULE MATCHED] '{_rule.__name__}'\n"
+                f"[INPUT] {kwargs if should_show_input else '<Skipping...>'}\n"
+                f"[RETURN] {result}\n"
+                f"---------"
+            )
+            results.append(result)
+            if take_first:
+                return result
+    return None if take_first and not len(results) else results
 
@@ -1586,7 +1588,8 @@

Source code in orbiter/rules/rulesets.py -
+ + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + +
288
+              
287
+288
 289
 290
 291
@@ -1645,68 +1648,67 @@ 

344 345 346 -347 -348

def apply_many(
-    self,
-    input_val: Collection[Any],
-    take_first: bool = False,
-) -> List[List[Any]] | List[Any]:
-    """
-    Apply a ruleset to each item in collection (such as `dict().items()`)
-    and return any results that are not `None`
-
-    You can turn the output of `apply_many` into a dict, if the rule takes and returns a tuple
-    ```pycon
-    >>> from itertools import chain
-    >>> from orbiter.rules import rule
-
-    >>> @rule
-    ... def filter_for_type_folder(val):
-    ...   (key, val) = val
-    ...   return (key, val) if val.get('Type', '') == 'Folder' else None
-    >>> ruleset = Ruleset(ruleset=[filter_for_type_folder])
-    >>> input_dict = {
-    ...    "a": {"Type": "Folder"},
-    ...    "b": {"Type": "File"},
-    ...    "c": {"Type": "Folder"},
-    ... }
-    >>> dict(chain(*chain(ruleset.apply_many(input_dict.items()))))
-    ... # use dict(chain(*chain(...))), if using `take_first=True`, to turn many results back into dict
-    {'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}
-    >>> dict(ruleset.apply_many(input_dict.items(), take_first=True))
-    ... # use dict(...) directly, if using `take_first=True`, to turn results back into dict
-    {'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}
-
-    ```
-
-    You cannot pass input without length
-    ```pycon
-    >>> ruleset.apply_many({})
-    ... # doctest: +IGNORE_EXCEPTION_DETAIL
-    Traceback (most recent call last):
-    RuntimeError: Input is not Collection[Any] with length!
-
-    ```
-    :param input_val: List to evaluate ruleset over
-    :type input_val: Collection[Any]
-    :param take_first: Only take the first (if any) result from each ruleset application
-    :type take_first: bool
-    :returns: List of list with all non-null evaluations for each item<br>
-              or list of the first non-null evaluation for each item (if `take_first=True`)
-    :rtype: List[List[Any]] | List[Any]
-    :raises RuntimeError: if the Ruleset or input_vals are empty
-    :raises RuntimeError: if the Rule raises an exception
-    """
-    # Validate Input
-    if not input_val or not len(input_val):
-        raise RuntimeError("Input is not `Collection[Any]` with length!")
-
-    return [
-        results[0] if take_first else results
-        for item in input_val
-        if (results := self.apply(take_first=False, val=item)) is not None
-        and len(results)
-    ]
+347
def apply_many(
+    self,
+    input_val: Collection[Any],
+    take_first: bool = False,
+) -> List[List[Any]] | List[Any]:
+    """
+    Apply a ruleset to each item in collection (such as `dict().items()`)
+    and return any results that are not `None`
+
+    You can turn the output of `apply_many` into a dict, if the rule takes and returns a tuple
+    ```pycon
+    >>> from itertools import chain
+    >>> from orbiter.rules import rule
+
+    >>> @rule
+    ... def filter_for_type_folder(val):
+    ...   (key, val) = val
+    ...   return (key, val) if val.get('Type', '') == 'Folder' else None
+    >>> ruleset = Ruleset(ruleset=[filter_for_type_folder])
+    >>> input_dict = {
+    ...    "a": {"Type": "Folder"},
+    ...    "b": {"Type": "File"},
+    ...    "c": {"Type": "Folder"},
+    ... }
+    >>> dict(chain(*chain(ruleset.apply_many(input_dict.items()))))
+    ... # use dict(chain(*chain(...))), if using `take_first=True`, to turn many results back into dict
+    {'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}
+    >>> dict(ruleset.apply_many(input_dict.items(), take_first=True))
+    ... # use dict(...) directly, if using `take_first=True`, to turn results back into dict
+    {'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}
+
+    ```
+
+    You cannot pass input without length
+    ```pycon
+    >>> ruleset.apply_many({})
+    ... # doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    RuntimeError: Input is not Collection[Any] with length!
+
+    ```
+    :param input_val: List to evaluate ruleset over
+    :type input_val: Collection[Any]
+    :param take_first: Only take the first (if any) result from each ruleset application
+    :type take_first: bool
+    :returns: List of list with all non-null evaluations for each item<br>
+              or list of the first non-null evaluation for each item (if `take_first=True`)
+    :rtype: List[List[Any]] | List[Any]
+    :raises RuntimeError: if the Ruleset or input_vals are empty
+    :raises RuntimeError: if the Rule raises an exception
+    """
+    # Validate Input
+    if not input_val or not len(input_val):
+        raise RuntimeError("Input is not `Collection[Any]` with length!")
+
+    return [
+        results[0] if take_first else results
+        for item in input_val
+        if (results := self.apply(take_first=False, val=item)) is not None
+        and len(results)
+    ]
 
@@ -2013,6 +2015,8 @@

+ + \ No newline at end of file diff --git a/Rules_and_Rulesets/template/index.html b/Rules_and_Rulesets/template/index.html index 686e43a..28fff89 100644 --- a/Rules_and_Rulesets/template/index.html +++ b/Rules_and_Rulesets/template/index.html @@ -48,6 +48,8 @@ + + @@ -989,6 +991,8 @@

Template

+ + \ No newline at end of file diff --git a/assets/_markdown_exec_pyodide.css b/assets/_markdown_exec_pyodide.css new file mode 100644 index 0000000..71f9f28 --- /dev/null +++ b/assets/_markdown_exec_pyodide.css @@ -0,0 +1,50 @@ +html[data-theme="light"] { + @import "https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow.css" +} + +html[data-theme="dark"] { + @import "https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow-night-blue.min.css" +} + + +.ace_gutter { + z-index: 1; +} + +.pyodide-editor { + width: 100%; + min-height: 200px; + max-height: 400px; + font-size: .85em; +} + +.pyodide-editor-bar { + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + width: 100%; + font: monospace; + font-size: 0.75em; + padding: 2px 0 2px; +} + +.pyodide-bar-item { + padding: 0 18px 0; + display: inline-block; + width: 50%; +} + +.pyodide pre { + margin: 0; +} + +.pyodide-output { + width: 100%; + margin-bottom: -15px; + min-height: 46px; + max-height: 400px +} + +.pyodide-clickable { + cursor: pointer; + text-align: right; +} \ No newline at end of file diff --git a/assets/_markdown_exec_pyodide.js b/assets/_markdown_exec_pyodide.js new file mode 100644 index 0000000..1f6ae91 --- /dev/null +++ b/assets/_markdown_exec_pyodide.js @@ -0,0 +1,109 @@ +var _sessions = {}; + +function getSession(name, pyodide) { + if (!(name in _sessions)) { + _sessions[name] = pyodide.globals.get("dict")(); + } + return _sessions[name]; +} + +function writeOutput(element, string) { + element.innerHTML += string + '\n'; +} + +function clearOutput(element) { + element.innerHTML = ''; +} + +async function evaluatePython(pyodide, editor, output, session) { + pyodide.setStdout({ batched: (string) => { writeOutput(output, string); } }); + let result, code = editor.getValue(); + clearOutput(output); + try { + result = await pyodide.runPythonAsync(code, { globals: getSession(session, pyodide) }); + } catch (error) { + writeOutput(output, error); + } + if (result) writeOutput(output, result); + hljs.highlightElement(output); +} + +async function initPyodide() { + try { + let pyodide = await loadPyodide(); + await pyodide.loadPackage("micropip"); + return pyodide; + } catch(error) { + return null; + } +} + +function getTheme() { + return document.body.getAttribute('data-md-color-scheme'); +} + +function setTheme(editor, currentTheme, light, dark) { + // https://gist.github.com/RyanNutt/cb8d60997d97905f0b2aea6c3b5c8ee0 + if (currentTheme === "default") { + editor.setTheme("ace/theme/" + light); + document.querySelector(`link[title="light"]`).removeAttribute("disabled"); + document.querySelector(`link[title="dark"]`).setAttribute("disabled", "disabled"); + } else if (currentTheme === "slate") { + editor.setTheme("ace/theme/" + dark); + document.querySelector(`link[title="dark"]`).removeAttribute("disabled"); + document.querySelector(`link[title="light"]`).setAttribute("disabled", "disabled"); + } +} + +function updateTheme(editor, light, dark) { + // Create a new MutationObserver instance + const observer = new MutationObserver((mutations) => { + // Loop through the mutations that occurred + mutations.forEach((mutation) => { + // Check if the mutation was a change to the data-md-color-scheme attribute + if (mutation.attributeName === 'data-md-color-scheme') { + // Get the new value of the attribute + const newColorScheme = mutation.target.getAttribute('data-md-color-scheme'); + // Update the editor theme + setTheme(editor, newColorScheme, light, dark); + } + }); + }); + + // Configure the observer to watch for changes to the data-md-color-scheme attribute + observer.observe(document.body, { + attributes: true, + attributeFilter: ['data-md-color-scheme'], + }); +} + +async function setupPyodide(idPrefix, install = null, themeLight = 'tomorrow', themeDark = 'tomorrow_night', session = null) { + const editor = ace.edit(idPrefix + "editor"); + const run = document.getElementById(idPrefix + "run"); + const clear = document.getElementById(idPrefix + "clear"); + const output = document.getElementById(idPrefix + "output"); + + updateTheme(editor, themeLight, themeDark); + + editor.session.setMode("ace/mode/python"); + setTheme(editor, getTheme(), themeLight, themeDark); + + writeOutput(output, "Initializing..."); + let pyodide = await pyodidePromise; + if (install && install.length) { + micropip = pyodide.pyimport("micropip"); + for (const package of install) + await micropip.install(package); + } + clearOutput(output); + run.onclick = () => evaluatePython(pyodide, editor, output, session); + clear.onclick = () => clearOutput(output); + output.parentElement.parentElement.addEventListener("keydown", (event) => { + if (event.ctrlKey && event.key.toLowerCase() === 'enter') { + event.preventDefault(); + run.click(); + } + }); +} + +var pyodidePromise = initPyodide(); diff --git a/cli/index.html b/cli/index.html index 00c3c58..79ca33f 100644 --- a/cli/index.html +++ b/cli/index.html @@ -48,6 +48,8 @@ + + @@ -293,6 +295,17 @@ + + @@ -303,6 +316,76 @@ + + + + @@ -739,31 +822,132 @@

CLI

-

orbiter

-

orbiter is a CLI that runs on your workstation -and converts workflow definitions from other tools to Airflow Projects.

+

orbiter

+

Orbiter is a CLI that converts other workflows to Airflow Projects.

Usage:

orbiter [OPTIONS] COMMAND [ARGS]...
 

Options:

-
  --version  Show the version and exit.
-  --help     Show this message and exit.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
NameTypeDescriptionDefault
--versionbooleanShow the version and exit.False
--helpbooleanShow this message and exit.False
+

help

+

List available Translation Rulesets

+

Usage:

+
orbiter help [OPTIONS]
+
+

Options:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
--helpbooleanShow this message and exit.False
+

install

+

Install a new Orbiter Translation Ruleset from a repository

+

Usage:

+
orbiter install [OPTIONS]
 
-

translate

-

Translate workflow artifacts in an INPUT_DIR folder -to an OUTPUT_DIR Airflow Project folder.

+

Options:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
-r, --repochoice (astronomer-orbiter-translations | orbiter-community-translations)Choose a repository to install (will prompt, if not given)None
-k, --keytext[Optional] License Key to use for the translation ruleset.
+

Should look like 'AAAA-BBBB-1111-2222-3333-XXXX-YYYY-ZZZZ' | None | +| --help | boolean | Show this message and exit. | False |

+

translate

+

Translate workflows in an INPUT_DIR to an OUTPUT_DIR Airflow Project.

Provide a specific ruleset with the --ruleset flag.

-

INPUT_DIR defaults to $CWD/workflow

+

Run orbiter help to see available rulesets.

+

INPUT_DIR defaults to $CWD/workflow.

OUTPUT_DIR defaults to $CWD/output

Usage:

-
orbiter translate [OPTIONS] INPUT_DIR OUTPUT_DIR
+
orbiter translate [OPTIONS] INPUT_DIR OUTPUT_DIR
 

Options:

-
  -r, --ruleset TEXT      Qualified name of a TranslationRuleset  [required]
-  --format / --no-format  [optional] format the output with Ruff  [default:
-                          format]
-  --help                  Show this message and exit.
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
-r, --rulesettextQualified name of a TranslationRuleset_required
--format / --no-formatboolean[optional] format the output with RuffTrue
--helpbooleanShow this message and exit.False

Logging

You can alter the verbosity of the CLI by setting the LOG_LEVEL environment variable. The default is INFO.

export LOG_LEVEL=DEBUG
@@ -827,6 +1011,8 @@ 

Logging + + \ No newline at end of file diff --git a/index.html b/index.html index a65d9a3..5345e13 100644 --- a/index.html +++ b/index.html @@ -46,6 +46,8 @@ + + @@ -861,14 +863,9 @@

Installationorbiter CLI are available for download on the Releases page.

Translate

-

You can utilize the orbiter CLI with pre-built translations to convert workflows +

You can utilize the orbiter CLI with existing translations to convert workflows from other systems to Apache Airflow.

-

The list of systems Orbiter has support for translating is listed at Origins

-

Use the orbiter translate command to convert workflows via a specific translation ruleset

    -
  1. Determine the specific translation ruleset via the Origins page, - or create a translation ruleset, - if one does not exist
  2. Set up a new folder, and create a workflow/ folder. Add your workflows files to it
    .
     └── workflow/
    @@ -876,7 +873,15 @@ 

    Translate ├── workflow_b.json └── ...

  3. -
  4. Invoke the orbiter CLI (replacing <RULESET> with your desired ruleset). This will produce output to an output/ folder: +
  5. Determine the specific translation ruleset via:
      +
    1. the Origins documentation
    2. +
    3. the orbiter help command
    4. +
    5. or by creating a translation ruleset, if one does not exist
    6. +
    +
  6. +
  7. Install the specific translation ruleset via the orbiter install command
  8. +
  9. Use the orbiter translate command with the <RULESET> determined in the last step + This will produce output to an output/ folder:
    orbiter translate workflow/ output/ --ruleset <RULESET>
     
  10. Review the contents of the output/ folder. If extensions or customizations are required, review @@ -943,7 +948,7 @@

    Extend or CustomizeOrigins page

  11. -
  12. Importing required Orbiter Objects
  13. +
  14. Importing required Objects
  15. Importing required Rule types
  16. Create one or more @rule functions, as required. A higher priority means this rule will be applied first. @task_rule Reference
  17. @@ -1046,6 +1051,8 @@

    FAQ¶< + + \ No newline at end of file diff --git a/objects/Tasks/Operators_and_Callbacks/callbacks/index.html b/objects/Tasks/Operators_and_Callbacks/callbacks/index.html index f96fa86..415aefe 100644 --- a/objects/Tasks/Operators_and_Callbacks/callbacks/index.html +++ b/objects/Tasks/Operators_and_Callbacks/callbacks/index.html @@ -48,6 +48,8 @@ + + @@ -1146,6 +1148,8 @@

    + + \ No newline at end of file diff --git a/objects/Tasks/Operators_and_Callbacks/operators/index.html b/objects/Tasks/Operators_and_Callbacks/operators/index.html index af0e24a..eeed605 100644 --- a/objects/Tasks/Operators_and_Callbacks/operators/index.html +++ b/objects/Tasks/Operators_and_Callbacks/operators/index.html @@ -46,6 +46,8 @@ + + @@ -908,6 +910,16 @@

    Operators

    +
    + +
    @@ -1885,6 +1897,8 @@

    + + \ No newline at end of file diff --git a/objects/Tasks/index.html b/objects/Tasks/index.html index de1c913..16d2695 100644 --- a/objects/Tasks/index.html +++ b/objects/Tasks/index.html @@ -48,6 +48,8 @@ + + @@ -881,7 +883,7 @@

    DiagramDiagram + + \ No newline at end of file diff --git a/objects/dags/index.html b/objects/dags/index.html index 0e8232c..f06aaee 100644 --- a/objects/dags/index.html +++ b/objects/dags/index.html @@ -48,6 +48,8 @@ + + @@ -862,7 +864,6 @@

    DiagramDiagramDiagramDiagramDiagram

+ }
@@ -1629,6 +1622,8 @@

+ + \ No newline at end of file diff --git a/objects/index.html b/objects/index.html index dde207e..10063a8 100644 --- a/objects/index.html +++ b/objects/index.html @@ -48,6 +48,8 @@ + + @@ -778,11 +780,11 @@

Overview

-

Orbiter objects are returned from Rules during a translation, and +

Objects are returned from Rules during a translation, and are rendered to produce an Apache Airflow Project

-

Diagram of Orbiter Translation

An OrbiterProject holds everything necessary to render an Airflow Project. -This is generated by a TranslationRuleset.translation_fn.

+This is generated by a TranslationRuleset.translate_fn.

+

Diagram of Orbiter Translation

Workflows are represented by a OrbiterDAG which is a Directed Acyclic Graph (of Tasks).

@@ -966,6 +968,8 @@

+ + \ No newline at end of file diff --git a/objects/project/index.html b/objects/project/index.html index 46ad84f..2498a3c 100644 --- a/objects/project/index.html +++ b/objects/project/index.html @@ -48,6 +48,8 @@ + + @@ -908,7 +910,7 @@

Project

An OrbiterProject holds everything necessary to render an Airflow Project. -This is generated by a TranslationRuleset.translation_fn.

+This is generated by a TranslationRuleset.translate_fn.

Diagram

classDiagram
     direction LR
@@ -929,14 +931,14 @@ 

DiagramDiagramDiagram

@@ -1571,7 +1572,7 @@

Holds everything necessary to render an Airflow Project. -This is generated by a TranslationRuleset.translation_fn.

+This is generated by a TranslationRuleset.translate_fn.

Tip

They can be added together @@ -3312,6 +3313,8 @@

+ + \ No newline at end of file diff --git a/orbiter_diagram.png b/orbiter_diagram.png index 2e67b5629aa14f26cf60bda514fd5d7d4c7e234f..d0a1145c8ee197ace3e13fe539c21a8700692aa7 100644 GIT binary patch literal 309139 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

9CR=Ej@exqq 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
"DAG"OriginMaintainerRepositoryRuleset(s)Task EquivalentDAG Equivalent
DAG FactoryCommunityorbiter_translations.dag_factory.yaml_base--DAG FactoryCommunityorbiter-community-translationsorbiter_translations.dag_factory.yaml_base.translation_ruleset------
Control MAstronomerorbiter_translations.control_m.json_baseJobFolderControl MAstronomerastronomer-orbiter-translationsorbiter_translations.control_m.json_base.translation_rulesetJobFolder
orbiter_translations.control_m.json_ssh⠀⠀⠀orbiter_translations.control_m.json_ssh.translation_ruleset⠀⠀
orbiter_translations.control_m.xml_base⠀⠀⠀orbiter_translations.control_m.xml_base.translation_ruleset⠀⠀
orbiter_translations.control_m.xml_ssh⠀⠀⠀orbiter_translations.control_m.xml_ssh.translation_ruleset⠀⠀
AutomicAstronomerJobJob PlanAutomicAstronomerastronomer-orbiter-translationsWIPJobJob Plan
AutosysAstronomerAutosysAstronomerastronomer-orbiter-translationsWIP⠀⠀
JAMSAstronomerJAMSAstronomerastronomer-orbiter-translationsWIPJobFolderâ €
SSISAstronomerorbiter_translations.ssis.xml_baseSSISAstronomerastronomer-orbiter-translationsorbiter_translations.ssis.xml_base.translation_ruleset⠀⠀
OozieAstronomerorbiter_translations.oozie.xml_baseOozieAstronomerastronomer-orbiter-translationsorbiter_translations.oozie.xml_base.translation_rulesetNodeWorkflow
& more!& more!⠀⠀⠀⠀⠀
@@ -934,6 +948,8 @@

Supported Origins + + \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json index 207afff..1798bee 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

Astronomer Orbiter can land legacy workloads safely down in a new home on Apache Airflow!

"},{"location":"#what-is-orbiter","title":"What is Orbiter?","text":"

Orbiter is both a CLI and Framework for converting workflows from other orchestration tools to Apache Airflow.

Generally it can be thoughts of as:

flowchart LR\n    origin{{ XML/JSON/YAML/Etc Workflows }}\n    origin -->| \u2728 Translations \u2728 | airflow{{ Apache Airflow Project }}
The framework is a set of Rules and Objects that can translate workflows from an Origin system to an Airflow project.

"},{"location":"#installation","title":"Installation","text":"

You can install the orbiter CLI, if you have Python >= 3.10 installed via pip:

pip install astronomer-orbiter\n
If you do not have a compatible Python environment, pre-built binary executables of the orbiter CLI are available for download on the Releases page.

"},{"location":"#translate","title":"Translate","text":"

You can utilize the orbiter CLI with pre-built translations to convert workflows from other systems to Apache Airflow.

The list of systems Orbiter has support for translating is listed at Origins

Use the orbiter translate command to convert workflows via a specific translation ruleset

  1. Determine the specific translation ruleset via the Origins page, or create a translation ruleset, if one does not exist
  2. Set up a new folder, and create a workflow/ folder. Add your workflows files to it
    .\n\u2514\u2500\u2500 workflow/\n    \u251c\u2500\u2500 workflow_a.json\n    \u251c\u2500\u2500 workflow_b.json\n    \u2514\u2500\u2500 ...\n
  3. Invoke the orbiter CLI (replacing <RULESET> with your desired ruleset). This will produce output to an output/ folder:
    orbiter translate workflow/ output/ --ruleset <RULESET>\n
  4. Review the contents of the output/ folder. If extensions or customizations are required, review how to extend a translation ruleset
  5. Utilize the astro CLI to run Airflow instance with your migrated workloads
  6. Deploy to Astro to run your translated workflows in production! \ud83d\ude80

You can see more specifics on how to use the Orbiter CLI in the CLI section.

"},{"location":"#authoring-rulesets-customization","title":"Authoring Rulesets & Customization","text":"

Orbiter can be extended to fit specific needs, patterns, or to support additional origins.

Read more specifics about how to use the framework at Rules and Objects

"},{"location":"#extend-or-customize","title":"Extend or Customize","text":"

To extend or customize an existing ruleset, you can easily modify it with simple Python code.

  1. Set up your workspace as described in steps 1+2 of the Translate instructions
  2. Create a Python script, named override.py
    .\n\u251c\u2500\u2500 override.py\n\u2514\u2500\u2500 workflow/\n    \u251c\u2500\u2500 workflow_a.json\n    \u251c\u2500\u2500 workflow_b.json\n    \u2514\u2500\u2500 ...\n
  3. Add contents to override.py: override.py

    from orbiter_community_translations.dag_factory import translation_ruleset  # (1)!\nfrom orbiter.objects.operators.ssh import OrbiterSSHOperator  # (2)!\nfrom orbiter.rules import task_rule  # (3)!\n\n\n@task_rule(priority=99)  # (4)!\ndef ssh_rule(val: dict):\n    \"\"\"Demonstration of overriding rulesets, by switching DAG Factory BashOperators to SSHOperators\"\"\"\n    if val.pop(\"operator\", \"\") == \"BashOperator\":  # (5)!\n        return OrbiterSSHOperator(  # (6)!\n            command=val.pop(\"bash_command\"),\n            doc=\"Hello World!\",\n            **{k: v for k, v in val if k != \"dependencies\"},\n        )\n    else:\n        return None\n\n\ntranslation_ruleset.task_ruleset.ruleset.append(ssh_rule)  # (7)!\n

    1. Importing specific translation ruleset, determined via the Origins page
    2. Importing required Orbiter Objects
    3. Importing required Rule types
    4. Create one or more @rule functions, as required. A higher priority means this rule will be applied first. @task_rule Reference
    5. Rules have an if/else statement - they must always return a single thing or nothing
    6. OrbiterSSHOperator Reference
    7. Append the new Rule to the translation_ruleset
  4. Invoke the orbiter CLI, pointing it at your customized ruleset, and writing output to an output/ folder:

    orbiter translate workflow/ output/ --ruleset override.translation_ruleset\n

  5. Follow the remaining steps 4 -> 6 of the Translate instructions
"},{"location":"#authoring-a-new-ruleset","title":"Authoring a new Ruleset","text":"

You can utilize the Template TranslationRuleset as a starter, to create a new TranslationRuleset.

"},{"location":"#faq","title":"FAQ","text":"
  • Can this tool convert my workflows from tool X to Airflow?

    If you don't see your tool listed in Supported Origins, contact us for services to create translations, create an issue in the orbiter-community-translations repository, or write a TranslationRuleset and submit a pull request to share your translations with the community.

  • Are the results of this tool under any guarantee of correctness?

    No. This tool is provided as-is, with no guarantee of correctness. It is your responsibility to verify the results. We accept Pull Requests to improve parsing, and strive to make the tool easily configurable to handle your specific use-case.

Artwork Orbiter logo by Ivan Colic used with permission from The Noun Project under Creative Commons.

"},{"location":"cli/","title":"CLI","text":""},{"location":"cli/#orbiter","title":"orbiter","text":"

orbiter is a CLI that runs on your workstation and converts workflow definitions from other tools to Airflow Projects.

Usage:

orbiter [OPTIONS] COMMAND [ARGS]...\n

Options:

  --version  Show the version and exit.\n  --help     Show this message and exit.\n
"},{"location":"cli/#translate","title":"translate","text":"

Translate workflow artifacts in an INPUT_DIR folder to an OUTPUT_DIR Airflow Project folder.

Provide a specific ruleset with the --ruleset flag.

INPUT_DIR defaults to $CWD/workflow

OUTPUT_DIR defaults to $CWD/output

Usage:

orbiter translate [OPTIONS] INPUT_DIR OUTPUT_DIR\n

Options:

  -r, --ruleset TEXT      Qualified name of a TranslationRuleset  [required]\n  --format / --no-format  [optional] format the output with Ruff  [default:\n                          format]\n  --help                  Show this message and exit.\n
"},{"location":"cli/#logging","title":"Logging","text":"

You can alter the verbosity of the CLI by setting the LOG_LEVEL environment variable. The default is INFO.

export LOG_LEVEL=DEBUG\n
"},{"location":"origins/","title":"Origins","text":"

An Origin is a source system that contains workflows that can be translated to an Apache Airflow project.

"},{"location":"origins/#supported-origins","title":"Supported Origins","text":"Origin Maintainer Ruleset(s) \"Task\" \"DAG\" DAG Factory Community orbiter_translations.dag_factory.yaml_base - - Control M Astronomer orbiter_translations.control_m.json_base Job Folder orbiter_translations.control_m.json_ssh orbiter_translations.control_m.xml_base orbiter_translations.control_m.xml_ssh Automic Astronomer Job Job Plan Autosys Astronomer JAMS Astronomer SSIS Astronomer orbiter_translations.ssis.xml_base Oozie Astronomer orbiter_translations.oozie.xml_base & more!

For Astronomer maintained Translation Rulesets, please contact us for access to the most up-to-date versions.

If you don't see your Origin system listed, please either:

  • contact us for services to create translations
  • create an issue in our orbiter-community-translations repository
  • write a TranslationRuleset and submit a pull request to share your translations with the community
"},{"location":"Rules_and_Rulesets/","title":"Overview","text":"

The \"brain\" of the Orbiter framework is in it's Rules and the Rulesets that contain them.

  • A Rule contains a python function that is evaluated and produces something (typically an Orbiter Object) or nothing
  • A Ruleset is a collection of Rules that are evaluated in priority order
  • A TranslationRuleset is a collection of Rulesets, relating to an Origin and File Type (e.g. .json, .xml, etc.), with a translation_fn (e.g. orbiter.rules.rulesets.translate) which determines how to apply the rulesets.

Different Rules are applied in different scenarios; such as for converting input to a DAG (@dag_rule), or a specific Airflow Operator (@task_rule), or for filtering entries from the input data (@dag_filter_rule, @task_filter_rule).

A Rule should evaluate to a single something or nothing.

Tip

If we want to map the following input

{\n    \"id\": \"my_task\",\n    \"command\": \"echo 'hi'\"\n}\n

to an Airflow BashOperator, a Rule could parse it as follows:

@task_rule\ndef my_rule(val):\n    if 'command' in val:\n        return OrbiterBashOperator(task_id=val['id'], bash_command=val['command'])\n

This returns a OrbiterBashOperator, which will become an Airflow BashOperator when the translation completes.

"},{"location":"Rules_and_Rulesets/#orbiter.rules.rulesets.translate","title":"orbiter.rules.rulesets.translate","text":"
translate(\n    translation_ruleset, input_dir: Path\n) -> OrbiterProject\n

Orbiter, by default, expects a folder containing text files (.json, .xml, .yaml, etc.) which may have a structure like:

{\"<workflow name>\": { ...<workflow properties>, \"<task name>\": { ...<task properties>} }}\n

The default translation function (orbiter.rules.rulesets.translate) performs the following steps:

  1. Look in the input folder for all files with the expected TranslationRuleset.file_type. For each file, it will:
    1. Load the file and turn it into a Python Dictionary
    2. Apply the TranslationRuleset.dag_filter_ruleset to filter down to keys suspected of being translatable to a DAG, in priority order. For each suspected DAG dict:
      1. Apply the TranslationRuleset.dag_ruleset, to convert the object to an OrbiterDAG, in priority-order, stopping when the first rule returns a match.
      2. Apply the TranslationRuleset.task_filter_ruleset to filter down to keys suspected of being translatable to a Task, in priority-order. For each suspected Task dict:
        1. Apply the TranslationRuleset.task_ruleset, in priority-order, stopping when the first rule returns a match, to convert the dictionary to a specific type of Task. If no rule returns a match, the dict is filtered.
      3. After the DAG and Tasks are mapped, the TranslationRuleset.task_dependency_ruleset is applied in priority-order, stopping when the first rule returns a match, to create a list of OrbiterTaskDependency, which are then added to each task in the OrbiterDAG
  2. Apply the TranslationRuleset.post_processing_ruleset, against the OrbiterProject, which can make modifications after all other rules have been applied.
  3. Return the OrbiterProject

After translation - the OrbiterProject is rendered to the output folder.

Source code in orbiter/rules/rulesets.py
@validate_call\ndef translate(translation_ruleset, input_dir: Path) -> OrbiterProject:\n    \"\"\"\n    Orbiter, by default, expects a folder containing text files (`.json`, `.xml`, `.yaml`, etc.)\n    which may have a structure like:\n    ```json\n    {\"<workflow name>\": { ...<workflow properties>, \"<task name>\": { ...<task properties>} }}\n    ```\n\n    The default translation function (`orbiter.rules.rulesets.translate`) performs the following steps:\n\n    1. Look in the input folder for all files with the expected\n    [`TranslationRuleset.file_type`][orbiter.rules.rulesets.TranslationRuleset].\n        For each file, it will:\n        1. Load the file and turn it into a Python Dictionary\n        2. Apply the [`TranslationRuleset.dag_filter_ruleset`][orbiter.rules.rulesets.DAGFilterRuleset]\n            to filter down to keys suspected of being translatable to a DAG,\n            in priority order. For each suspected DAG dict:\n            1. Apply the [`TranslationRuleset.dag_ruleset`][orbiter.rules.rulesets.DAGRuleset],\n                to convert the object to an [`OrbiterDAG`][orbiter.objects.dag.OrbiterDAG],\n                in priority-order, stopping when the first rule returns a match.\n            2. Apply the [`TranslationRuleset.task_filter_ruleset`][orbiter.rules.rulesets.TaskFilterRuleset]\n                to filter down to keys suspected of being translatable to a Task,\n                in priority-order. For each suspected Task dict:\n                1. Apply the [`TranslationRuleset.task_ruleset`][orbiter.rules.rulesets.TaskRuleset],\n                    in priority-order, stopping when the first rule returns a match,\n                    to convert the dictionary to a specific type of Task. If no rule returns a match,\n                    the dict is filtered.\n            3. After the DAG and Tasks are mapped, the\n                [`TranslationRuleset.task_dependency_ruleset`][orbiter.rules.rulesets.TaskDependencyRuleset]\n                is applied in priority-order, stopping when the first rule returns a match,\n                to create a list of\n                [`OrbiterTaskDependency`][orbiter.objects.task.OrbiterTaskDependency],\n                which are then added to each task in the\n                [`OrbiterDAG`][orbiter.objects.dag.OrbiterDAG]\n    2. Apply the [`TranslationRuleset.post_processing_ruleset`][orbiter.rules.rulesets.PostProcessingRuleset],\n        against the [`OrbiterProject`][orbiter.objects.project.OrbiterProject], which can make modifications after all\n        other rules have been applied.\n    3. Return the [`OrbiterProject`][orbiter.objects.project.OrbiterProject]\n\n    After translation - the [`OrbiterProject`][orbiter.objects.project.OrbiterProject] is rendered to the output folder.\n    \"\"\"\n\n    def _get_files_with_extension(_extension: str, _input_dir: Path) -> List[Path]:\n        return [\n            directory / file\n            for (directory, _, files) in _input_dir.walk()\n            for file in files\n            if _extension == file.lower()[-len(_extension) :]\n        ]\n\n    if not isinstance(translation_ruleset, TranslationRuleset):\n        raise RuntimeError(\n            f\"Error! type(translation_ruleset)=={type(translation_ruleset)}!=TranslationRuleset! Exiting!\"\n        )\n\n    # Create an initial OrbiterProject\n    project = OrbiterProject()\n\n    extension = translation_ruleset.file_type.value.lower()\n\n    logger.info(f\"Finding files with extension={extension} in {input_dir}\")\n    files = _get_files_with_extension(extension, input_dir)\n\n    # .yaml is sometimes '.yml'\n    if extension == \"yaml\":\n        files.extend(_get_files_with_extension(\"yml\", input_dir))\n\n    logger.info(f\"Found {len(files)} files with extension={extension} in {input_dir}\")\n\n    for file in files:\n        logger.info(f\"Translating file={file.resolve()}\")\n\n        # Load the file and convert it into a python dict\n        input_dict = load_filetype(file.read_text(), translation_ruleset.file_type)\n\n        # DAG FILTER Ruleset - filter down to keys suspected of being translatable to a DAG, in priority order.\n        dag_dicts = functools.reduce(\n            add,\n            translation_ruleset.dag_filter_ruleset.apply(val=input_dict),\n            [],\n        )\n        logger.debug(f\"Found {len(dag_dicts)} DAG candidates in {file.resolve()}\")\n        for dag_dict in dag_dicts:\n            # DAG Ruleset - convert the object to an `OrbiterDAG` via `dag_ruleset`,\n            #         in priority-order, stopping when the first rule returns a match\n            dag: OrbiterDAG | None = translation_ruleset.dag_ruleset.apply(\n                val=dag_dict,\n                take_first=True,\n            )\n            if dag is None:\n                logger.warning(\n                    f\"Couldn't extract DAG from dag_dict={dag_dict} with dag_ruleset={translation_ruleset.dag_ruleset}\"\n                )\n                continue\n            dag.orbiter_kwargs[\"file_path\"] = str(file.resolve())\n\n            tasks = {}\n            # TASK FILTER Ruleset - Many entries in dag_dict -> Many task_dict\n            task_dicts = functools.reduce(\n                add,\n                translation_ruleset.task_filter_ruleset.apply(val=dag_dict),\n                [],\n            )\n            logger.debug(\n                f\"Found {len(task_dicts)} Task candidates in {dag.dag_id} in {file.resolve()}\"\n            )\n            for task_dict in task_dicts:\n                # TASK Ruleset one -> one\n                task: OrbiterOperator = translation_ruleset.task_ruleset.apply(\n                    val=task_dict, take_first=True\n                )\n                if task is None:\n                    logger.warning(\n                        f\"Couldn't extract task from expected task_dict={task_dict}\"\n                    )\n                    continue\n\n                _add_task_deduped(task, tasks)\n            logger.debug(f\"Adding {len(tasks)} tasks to DAG {dag.dag_id}\")\n            dag.add_tasks(tasks.values())\n\n            # Dag-Level TASK DEPENDENCY Ruleset\n            task_dependencies: List[OrbiterTaskDependency] = (\n                list(chain(*translation_ruleset.task_dependency_ruleset.apply(val=dag)))\n                or []\n            )\n            if not len(task_dependencies):\n                logger.warning(f\"Couldn't find task dependencies in dag={dag_dict}\")\n            for task_dependency in task_dependencies:\n                task_dependency: OrbiterTaskDependency\n                if task_dependency.task_id not in dag.tasks:\n                    logger.warning(\n                        f\"Couldn't find task_id={task_dependency.task_id} in tasks={tasks} for dag_id={dag.dag_id}\"\n                    )\n                    continue\n                else:\n                    dag.tasks[task_dependency.task_id].add_downstream(task_dependency)\n\n            logger.debug(f\"Adding DAG {dag.dag_id} to project\")\n            project.add_dags(dag)\n\n    # POST PROCESSING Ruleset\n    translation_ruleset.post_processing_ruleset.apply(val=project, take_first=False)\n\n    return project\n
"},{"location":"Rules_and_Rulesets/#orbiter.rules.rulesets.load_filetype","title":"orbiter.rules.rulesets.load_filetype","text":"
load_filetype(input_str: str, file_type: FileType) -> dict\n

Orbiter converts all file types into a Python dictionary \"intermediate representation\" form, prior to any rulesets being applied.

FileType Conversion Method XML xmltodict_parse YAML yaml.safe_load JSON json.loads Source code in orbiter/rules/rulesets.py
@validate_call\ndef load_filetype(input_str: str, file_type: FileType) -> dict:\n    \"\"\"\n    Orbiter converts all file types into a Python dictionary \"intermediate representation\" form,\n    prior to any rulesets being applied.\n\n    | FileType | Conversion Method                                           |\n    |----------|-------------------------------------------------------------|\n    | `XML`    | [`xmltodict_parse`][orbiter.rules.rulesets.xmltodict_parse] |\n    | `YAML`   | `yaml.safe_load`                                            |\n    | `JSON`   | `json.loads`                                                |\n    \"\"\"\n\n    if file_type == FileType.JSON:\n        import json\n\n        return json.loads(input_str)\n    elif file_type == FileType.YAML:\n        import yaml\n\n        return yaml.safe_load(input_str)\n    elif file_type == FileType.XML:\n        return xmltodict_parse(input_str)\n    else:\n        raise NotImplementedError(f\"Cannot load file_type={file_type}\")\n
"},{"location":"Rules_and_Rulesets/#orbiter.rules.rulesets.xmltodict_parse","title":"orbiter.rules.rulesets.xmltodict_parse","text":"
xmltodict_parse(input_str: str) -> Any\n

Calls xmltodict.parse and does post-processing fixes.

Note

The original xmltodict.parse method returns EITHER:

  • a dict (one child element of type)
  • or a list of dict (many child element of type)

This behavior can be confusing, and is an issue with the original xml spec being referenced.

This method deviates by standardizing to the latter case (always a list[dict]).

All XML elements will be a list of dictionaries, even if there's only one element.

>>> xmltodict_parse(\"\")\nTraceback (most recent call last):\nxml.parsers.expat.ExpatError: no element found: line 1, column 0\n>>> xmltodict_parse(\"<a></a>\")\n{'a': None}\n>>> xmltodict_parse(\"<a foo='bar'></a>\")\n{'a': [{'@foo': 'bar'}]}\n>>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'></foo></a>\")  # Singleton - gets modified\n{'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}]}]}\n>>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'><bar><bop></bop></bar></foo></a>\")  # Nested Singletons - modified\n{'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz', 'bar': [{'bop': None}]}]}]}\n>>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'></foo><foo bing='bop'></foo></a>\")\n{'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}, {'@bing': 'bop'}]}]}\n

Parameters:

Name Type Description input_str str

The XML string to parse

Returns:

Type Description dict

The parsed XML

Source code in orbiter/rules/rulesets.py
def xmltodict_parse(input_str: str) -> Any:\n    \"\"\"Calls `xmltodict.parse` and does post-processing fixes.\n\n    !!! note\n\n        The original [`xmltodict.parse`](https://pypi.org/project/xmltodict/) method returns EITHER:\n\n        - a dict (one child element of type)\n        - or a list of dict (many child element of type)\n\n        This behavior can be confusing, and is an issue with the original xml spec being referenced.\n\n        **This method deviates by standardizing to the latter case (always a `list[dict]`).**\n\n        **All XML elements will be a list of dictionaries, even if there's only one element.**\n\n    ```pycon\n    >>> xmltodict_parse(\"\")\n    Traceback (most recent call last):\n    xml.parsers.expat.ExpatError: no element found: line 1, column 0\n    >>> xmltodict_parse(\"<a></a>\")\n    {'a': None}\n    >>> xmltodict_parse(\"<a foo='bar'></a>\")\n    {'a': [{'@foo': 'bar'}]}\n    >>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'></foo></a>\")  # Singleton - gets modified\n    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}]}]}\n    >>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'><bar><bop></bop></bar></foo></a>\")  # Nested Singletons - modified\n    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz', 'bar': [{'bop': None}]}]}]}\n    >>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'></foo><foo bing='bop'></foo></a>\")\n    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}, {'@bing': 'bop'}]}]}\n\n    ```\n    :param input_str: The XML string to parse\n    :type input_str: str\n    :return: The parsed XML\n    :rtype: dict\n    \"\"\"\n    import xmltodict\n\n    # noinspection t\n    def _fix(d):\n        \"\"\"fix the dict in place, recursively, standardizing on a list of dict even if there's only one entry.\"\"\"\n        # if it's a dict, descend to fix\n        if isinstance(d, dict):\n            for k, v in d.items():\n                # @keys are properties of elements, non-@keys are elements\n                if not k.startswith(\"@\"):\n                    if isinstance(v, dict):\n                        # THE FIX\n                        # any non-@keys should be a list of dict, even if there's just one of the element\n                        d[k] = [v]\n                        _fix(v)\n                    else:\n                        _fix(v)\n        # if it's a list, descend to fix\n        if isinstance(d, list):\n            for v in d:\n                _fix(v)\n\n    output = xmltodict.parse(input_str)\n    _fix(output)\n    return output\n
"},{"location":"Rules_and_Rulesets/rules/","title":"Rules","text":""},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.Rule","title":"orbiter.rules.Rule","text":"

A Rule contains a python function that is evaluated and produces something (typically an Orbiter Object) or nothing

A Rule can be created from a decorator

>>> @rule(priority=1)\n... def my_rule(val):\n...     return 1\n>>> isinstance(my_rule, Rule)\nTrue\n>>> my_rule(val={})\n1\n

The function in a rule takes one parameter (val), and must always evaluate to something or nothing.

>>> Rule(rule=lambda val: 4)({})\n4\n>>> Rule(rule=lambda val: None)({})\n

Tip

If the returned value is an Orbiter Object, the passed kwargs are saved in a special orbiter_kwargs property

>>> from orbiter.objects.dag import OrbiterDAG\n>>> @rule\n... def my_rule(foo):\n...     return OrbiterDAG(dag_id=\"\", file_path=\"\")\n>>> my_rule(foo=\"bar\").orbiter_kwargs\n{'foo': 'bar'}\n

Note

A Rule must have a rule property and extra properties cannot be passed

>>> # noinspection Pydantic\n... Rule(rule=lambda: None, not_a_prop=\"???\")\n... # doctest: +ELLIPSIS\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description rule Callable[[dict | Any], Any | None]

Python function to evaluate. Takes a single argument and returns something or nothing

priority int, optional

Higher priority rules are evaluated first, must be greater than 0. Default is 0

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.DAGFilterRule","title":"orbiter.rules.DAGFilterRule","text":"

Bases: Rule

The @dag_filter_rule decorator creates a DAGFilterRule

@dag_filter_rule\ndef foo(val: dict) -> List[dict]:\n    return [{\"dag_id\": \"foo\"}]\n

Hint

In addition to filtering, a DAGFilterRule can also map input to a more reasonable output for later processing

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.DAGRule","title":"orbiter.rules.DAGRule","text":"

Bases: Rule

A @dag_rule decorator creates a DAGRule

@dag_rule\ndef foo(val: dict) -> List[dict]:\n    return OrbiterDAG(dag_id=\"foo\")\n
"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.TaskFilterRule","title":"orbiter.rules.TaskFilterRule","text":"

Bases: Rule

A @task_filter_rule decorator creates a TaskFilterRule

@task_filter_rule\ndef foo(val: dict) -> List[dict]:\n    return [{\"task_id\": \"foo\"}]\n

Hint

In addition to filtering, a TaskFilterRule can also map input to a more reasonable output for later processing

Parameters:

Name Type Description val dict

A dictionary of the task

Returns:

Type Description List[dict] | None

A list of dictionaries of possible tasks or None

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.TaskRule","title":"orbiter.rules.TaskRule","text":"

Bases: Rule

A @task_rule decorator creates a TaskRule

@task_rule\ndef foo(val: dict) -> OrbiterOperator | OrbiterTaskGroup:\n    return OrbiterOperator(task_id=\"foo\")\n

Parameters:

Name Type Description val dict

A dictionary of the task

Returns:

Type Description OrbiterOperator | OrbiterTaskGroup | None

A subclass of OrbiterOperator or OrbiterTaskGroup or None

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.TaskDependencyRule","title":"orbiter.rules.TaskDependencyRule","text":"

Bases: Rule

An @task_dependency_rule decorator creates a TaskDependencyRule, which takes an OrbiterDAG and returns a list[OrbiterTaskDependency] or None

@task_dependency_rule\ndef foo(val: OrbiterDAG) -> OrbiterTaskDependency:\n    return [OrbiterTaskDependency(task_id=\"task_id\", downstream=\"downstream\")]\n

Parameters:

Name Type Description val OrbiterDAG

An OrbiterDAG

Returns:

Type Description List[OrbiterTaskDependency] | None

A list of OrbiterTaskDependency or None

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.PostProcessingRule","title":"orbiter.rules.PostProcessingRule","text":"

Bases: Rule

An @post_processing_rule decorator creates a PostProcessingRule, which takes an OrbiterProject, after all other rules have been applied, and modifies it in-place.

@post_processing_rule\ndef foo(val: OrbiterProject) -> None:\n    val.dags[\"foo\"].tasks[\"bar\"].description = \"Hello World\"\n

Parameters:

Name Type Description val OrbiterProject

An OrbiterProject

Returns:

Type Description None

None

"},{"location":"Rules_and_Rulesets/rulesets/","title":"Rulesets","text":""},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.TranslationRuleset","title":"orbiter.rules.rulesets.TranslationRuleset","text":"

A Ruleset is a collection of Rules that are evaluated in priority order

A TranslationRuleset is a container for Rulesets, which applies to a specific translation

>>> TranslationRuleset(\n...   file_type=FileType.JSON,                                      # Has a file type\n...   translate_fn=fake_translate,                                  # and can have a callable\n...   # translate_fn=\"orbiter.rules.translate.fake_translate\",      # or a qualified name to a function\n...   dag_filter_ruleset={\"ruleset\": [{\"rule\": lambda x: None}]},   # Rulesets can be dict within dicts\n...   dag_ruleset=DAGRuleset(ruleset=[Rule(rule=lambda x: None)]),  # or objects within objects\n...   task_filter_ruleset=EMPTY_RULESET,                            # or a mix\n...   task_ruleset=EMPTY_RULESET,\n...   task_dependency_ruleset=EMPTY_RULESET,                        # Omitted for brevity\n...   post_processing_ruleset=EMPTY_RULESET,\n... )\nTranslationRuleset(...)\n

Parameters:

Name Type Description file_type FileType

FileType to translate (.json, .xml, .yaml, etc.)

dag_filter_ruleset DAGFilterRuleset | dict

DAGFilterRuleset (of DAGFilterRule)

dag_ruleset DAGRuleset | dict

DAGRuleset (of DAGRules)

task_filter_ruleset TaskFilterRuleset | dict

TaskFilterRule (of TaskFilterRule)

task_ruleset TaskRuleset | dict

TaskRuleset (of TaskRules)

task_dependency_ruleset TaskDependencyRuleset | dict

TaskDependencyRuleset (of TaskDependencyRules)

post_processing_ruleset PostProcessingRuleset | dict

PostProcessingRuleset (of PostProcessingRules)

translate_fn Callable[[TranslationRuleset, Path], OrbiterProject] | str | TranslateFn

Either a qualified name to a function (e.g. path.to.file.function), or a function reference, with the signature: (translation_ruleset: Translation Ruleset, input_dir: Path) -> OrbiterProject

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.Ruleset","title":"orbiter.rules.rulesets.Ruleset","text":"

A list of rules, which are evaluated to generate different types of output

You must pass a Rule (or dict with the schema of Rule)

>>> from orbiter.rules import rule\n>>> @rule\n... def x(val):\n...    return None\n>>> Ruleset(ruleset=[x, {\"rule\": lambda: None}])\n... # doctest: +ELLIPSIS\nRuleset(ruleset=[Rule(...), Rule(...)])\n

Note

You can't pass non-Rules

>>> # noinspection PyTypeChecker\n... Ruleset(ruleset=[None])\n... # doctest: +ELLIPSIS\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description ruleset List[Rule | Callable[[Any], Any | None]]

List of Rule (or dict with the schema of Rule)

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.Ruleset.apply","title":"apply","text":"
apply(\n    take_first: bool = False, **kwargs\n) -> List[Any] | Any\n

Apply all rules in ruleset to a single item, in priority order, removing any None results.

A ruleset with one rule can produce up to one result

>>> from orbiter.rules import rule\n\n>>> @rule\n... def gt_4(val):\n...     return str(val) if val > 4 else None\n>>> Ruleset(ruleset=[gt_4]).apply(val=5)\n['5']\n

Many rules can produce many results, one for each rule.

>>> @rule\n... def gt_3(val):\n...    return str(val) if val > 3 else None\n>>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5)\n['5', '5']\n

The take_first flag will evaluate rules in the ruleset and return the first match

>>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5, take_first=True)\n'5'\n

If nothing matched, an empty list is returned

>>> @rule\n... def always_none(val):\n...     return None\n>>> @rule\n... def more_always_none(val):\n...     return None\n>>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5)\n[]\n

If nothing matched, and take_first=True, None is returned

>>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5, take_first=True)\n... # None\n

Tip

If no input is given, an error is returned

>>> Ruleset(ruleset=[always_none]).apply()\nTraceback (most recent call last):\nRuntimeError: No values provided! Supply at least one key=val pair as kwargs!\n

Parameters:

Name Type Description take_first bool

only take the first (if any) result from the ruleset application

kwargs

key=val pairs to pass to the evaluated rule function

Returns:

Type Description List[Any] | Any | None

List of rules that evaluated to Any (in priority order), or an empty list, or Any (if take_first=True)

Raises:

Type Description RuntimeError

if the Ruleset is empty or input_val is None

RuntimeError

if the Rule raises an exception

Source code in orbiter/rules/rulesets.py
@validate_call\ndef apply(self, take_first: bool = False, **kwargs) -> List[Any] | Any:\n    \"\"\"\n    Apply all rules in ruleset **to a single item**, in priority order, removing any `None` results.\n\n    A ruleset with one rule can produce **up to one** result\n    ```pycon\n    >>> from orbiter.rules import rule\n\n    >>> @rule\n    ... def gt_4(val):\n    ...     return str(val) if val > 4 else None\n    >>> Ruleset(ruleset=[gt_4]).apply(val=5)\n    ['5']\n\n    ```\n\n    Many rules can produce many results, one for each rule.\n    ```pycon\n    >>> @rule\n    ... def gt_3(val):\n    ...    return str(val) if val > 3 else None\n    >>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5)\n    ['5', '5']\n\n    ```\n\n    The `take_first` flag will evaluate rules in the ruleset and return the first match\n    ```pycon\n    >>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5, take_first=True)\n    '5'\n\n    ```\n\n    If nothing matched, an empty list is returned\n    ```pycon\n    >>> @rule\n    ... def always_none(val):\n    ...     return None\n    >>> @rule\n    ... def more_always_none(val):\n    ...     return None\n    >>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5)\n    []\n\n    ```\n\n    If nothing matched, and `take_first=True`, `None` is returned\n    ```pycon\n    >>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5, take_first=True)\n    ... # None\n\n    ```\n\n    !!! tip\n\n        If no input is given, an error is returned\n        ```pycon\n        >>> Ruleset(ruleset=[always_none]).apply()\n        Traceback (most recent call last):\n        RuntimeError: No values provided! Supply at least one key=val pair as kwargs!\n\n        ```\n\n    :param take_first: only take the first (if any) result from the ruleset application\n    :type take_first: bool\n    :param kwargs: key=val pairs to pass to the evaluated rule function\n    :returns: List of rules that evaluated to `Any` (in priority order),\n                or an empty list,\n                or `Any` (if `take_first=True`)\n    :rtype: List[Any] | Any | None\n    :raises RuntimeError: if the Ruleset is empty or input_val is None\n    :raises RuntimeError: if the Rule raises an exception\n    \"\"\"\n    if not len(kwargs):\n        raise RuntimeError(\n            \"No values provided! Supply at least one key=val pair as kwargs!\"\n        )\n    results = []\n    for _rule in self._sorted():\n        result = _rule(**kwargs)\n        should_show_input = \"val\" in kwargs and not (\n            isinstance(kwargs[\"val\"], OrbiterProject)\n            or isinstance(kwargs[\"val\"], OrbiterDAG)\n        )\n        if result is not None:\n            logger.debug(\n                \"---------\\n\"\n                f\"[RULESET MATCHED] '{self.__class__.__module__}.{self.__class__.__name__}'\\n\"\n                f\"[RULE MATCHED] '{_rule.__name__}'\\n\"\n                f\"[INPUT] {kwargs if should_show_input else '<Skipping...>'}\\n\"\n                f\"[RETURN] {result}\\n\"\n                f\"---------\"\n            )\n            results.append(result)\n            if take_first:\n                return result\n    return None if take_first and not len(results) else results\n
"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.Ruleset.apply_many","title":"apply_many","text":"
apply_many(\n    input_val: Collection[Any], take_first: bool = False\n) -> List[List[Any]] | List[Any]\n

Apply a ruleset to each item in collection (such as dict().items()) and return any results that are not None

You can turn the output of apply_many into a dict, if the rule takes and returns a tuple

>>> from itertools import chain\n>>> from orbiter.rules import rule\n\n>>> @rule\n... def filter_for_type_folder(val):\n...   (key, val) = val\n...   return (key, val) if val.get('Type', '') == 'Folder' else None\n>>> ruleset = Ruleset(ruleset=[filter_for_type_folder])\n>>> input_dict = {\n...    \"a\": {\"Type\": \"Folder\"},\n...    \"b\": {\"Type\": \"File\"},\n...    \"c\": {\"Type\": \"Folder\"},\n... }\n>>> dict(chain(*chain(ruleset.apply_many(input_dict.items()))))\n... # use dict(chain(*chain(...))), if using `take_first=True`, to turn many results back into dict\n{'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}\n>>> dict(ruleset.apply_many(input_dict.items(), take_first=True))\n... # use dict(...) directly, if using `take_first=True`, to turn results back into dict\n{'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}\n

You cannot pass input without length

>>> ruleset.apply_many({})\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\nRuntimeError: Input is not Collection[Any] with length!\n

Parameters:

Name Type Description input_val Collection[Any]

List to evaluate ruleset over

take_first bool

Only take the first (if any) result from each ruleset application

Returns:

Type Description List[List[Any]] | List[Any]

List of list with all non-null evaluations for each item or list of the first non-null evaluation for each item (if take_first=True)

Raises:

Type Description RuntimeError

if the Ruleset or input_vals are empty

RuntimeError

if the Rule raises an exception

Source code in orbiter/rules/rulesets.py
def apply_many(\n    self,\n    input_val: Collection[Any],\n    take_first: bool = False,\n) -> List[List[Any]] | List[Any]:\n    \"\"\"\n    Apply a ruleset to each item in collection (such as `dict().items()`)\n    and return any results that are not `None`\n\n    You can turn the output of `apply_many` into a dict, if the rule takes and returns a tuple\n    ```pycon\n    >>> from itertools import chain\n    >>> from orbiter.rules import rule\n\n    >>> @rule\n    ... def filter_for_type_folder(val):\n    ...   (key, val) = val\n    ...   return (key, val) if val.get('Type', '') == 'Folder' else None\n    >>> ruleset = Ruleset(ruleset=[filter_for_type_folder])\n    >>> input_dict = {\n    ...    \"a\": {\"Type\": \"Folder\"},\n    ...    \"b\": {\"Type\": \"File\"},\n    ...    \"c\": {\"Type\": \"Folder\"},\n    ... }\n    >>> dict(chain(*chain(ruleset.apply_many(input_dict.items()))))\n    ... # use dict(chain(*chain(...))), if using `take_first=True`, to turn many results back into dict\n    {'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}\n    >>> dict(ruleset.apply_many(input_dict.items(), take_first=True))\n    ... # use dict(...) directly, if using `take_first=True`, to turn results back into dict\n    {'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}\n\n    ```\n\n    You cannot pass input without length\n    ```pycon\n    >>> ruleset.apply_many({})\n    ... # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n    RuntimeError: Input is not Collection[Any] with length!\n\n    ```\n    :param input_val: List to evaluate ruleset over\n    :type input_val: Collection[Any]\n    :param take_first: Only take the first (if any) result from each ruleset application\n    :type take_first: bool\n    :returns: List of list with all non-null evaluations for each item<br>\n              or list of the first non-null evaluation for each item (if `take_first=True`)\n    :rtype: List[List[Any]] | List[Any]\n    :raises RuntimeError: if the Ruleset or input_vals are empty\n    :raises RuntimeError: if the Rule raises an exception\n    \"\"\"\n    # Validate Input\n    if not input_val or not len(input_val):\n        raise RuntimeError(\"Input is not `Collection[Any]` with length!\")\n\n    return [\n        results[0] if take_first else results\n        for item in input_val\n        if (results := self.apply(take_first=False, val=item)) is not None\n        and len(results)\n    ]\n
"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.DAGFilterRuleset","title":"orbiter.rules.rulesets.DAGFilterRuleset","text":"

Bases: Ruleset

Ruleset of DAGFilterRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.DAGRuleset","title":"orbiter.rules.rulesets.DAGRuleset","text":"

Bases: Ruleset

Ruleset of DAGRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.TaskFilterRuleset","title":"orbiter.rules.rulesets.TaskFilterRuleset","text":"

Bases: Ruleset

Ruleset of TaskFilterRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.TaskRuleset","title":"orbiter.rules.rulesets.TaskRuleset","text":"

Bases: Ruleset

Ruleset of TaskRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.TaskDependencyRuleset","title":"orbiter.rules.rulesets.TaskDependencyRuleset","text":"

Bases: Ruleset

Ruleset of TaskDependencyRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.PostProcessingRuleset","title":"orbiter.rules.rulesets.PostProcessingRuleset","text":"

Bases: Ruleset

Ruleset of PostProcessingRule

"},{"location":"Rules_and_Rulesets/template/","title":"Template","text":"

The following template can be utilized for creating a new TranslationRuleset

translation_template.py
from __future__ import annotations\nfrom orbiter import FileType\nfrom orbiter.objects.dag import OrbiterDAG\nfrom orbiter.objects.operators.empty import OrbiterEmptyOperator\nfrom orbiter.objects.project import OrbiterProject\nfrom orbiter.objects.task import OrbiterOperator\nfrom orbiter.objects.task_group import OrbiterTaskGroup\nfrom orbiter.rules import (\n    dag_filter_rule,\n    dag_rule,\n    task_filter_rule,\n    task_rule,\n    task_dependency_rule,\n    post_processing_rule,\n)\nfrom orbiter.rules.rulesets import (\n    DAGFilterRuleset,\n    DAGRuleset,\n    TaskFilterRuleset,\n    TaskRuleset,\n    TaskDependencyRuleset,\n    PostProcessingRuleset,\n    TranslationRuleset,\n)\n\n\n@dag_filter_rule\ndef basic_dag_filter(val: dict) -> list | None:\n    \"\"\"Filter input down to a list of dictionaries that can be processed by the `@dag_rules`\"\"\"\n    for k, v in val.items():\n        pass\n    return []\n\n\n@dag_rule\ndef basic_dag_rule(val: dict) -> OrbiterDAG | None:\n    \"\"\"Translate input into an `OrbiterDAG`\"\"\"\n    if \"dag_id\" in val:\n        return OrbiterDAG(dag_id=val[\"dag_id\"], file_path=\"file.py\")\n    else:\n        return None\n\n\n@task_filter_rule\ndef basic_task_filter(val: dict) -> list | None:\n    \"\"\"Filter input down to a list of dictionaries that can be processed by the `@task_rules`\"\"\"\n    for k, v in val.items():\n        pass\n    return []\n\n\n@task_rule(priority=2)\ndef basic_task_rule(val: dict) -> OrbiterOperator | OrbiterTaskGroup | None:\n    \"\"\"Translate input into an Operator (e.g. `OrbiterBashOperator`). will be applied first, with a higher priority\"\"\"\n    if \"task_id\" in val:\n        return OrbiterEmptyOperator(task_id=val[\"task_id\"])\n    else:\n        return None\n\n\n@task_rule(priority=1)\ndef cannot_map_rule(val: dict) -> OrbiterOperator | OrbiterTaskGroup | None:\n    \"\"\"This rule returns an `OrbiterEmptyOperator` with a doc string that says it cannot map the task,\n    so we can still see the task in the output. With a priority=1 it will be applied last\n    \"\"\"\n    import json\n\n    # noinspection PyArgumentList\n    return OrbiterEmptyOperator(\n        task_id=val[\"task_id\"], doc_md=f\"Cannot map task! input: {json.dumps(val)}\"\n    )\n\n\n@task_dependency_rule\ndef basic_task_dependency_rule(val: OrbiterDAG) -> list | None:\n    \"\"\"Translate input into a list of task dependencies\"\"\"\n    for task_dependency in val.orbiter_kwargs[\"task_dependencies\"]:\n        pass\n    return []\n\n\n@post_processing_rule\ndef basic_post_processing_rule(val: OrbiterProject) -> None:\n    \"\"\"Modify the project in-place, after all other rules have applied\"\"\"\n    for dag_id, dag in val.dags.items():\n        for task_id, task in dag.tasks.items():\n            pass\n\n\ntranslation_ruleset = TranslationRuleset(\n    file_type=FileType.JSON,\n    dag_filter_ruleset=DAGFilterRuleset(ruleset=[basic_dag_filter]),\n    dag_ruleset=DAGRuleset(ruleset=[basic_dag_rule]),\n    task_filter_ruleset=TaskFilterRuleset(ruleset=[basic_task_filter]),\n    task_ruleset=TaskRuleset(ruleset=[basic_task_rule, cannot_map_rule]),\n    task_dependency_ruleset=TaskDependencyRuleset(ruleset=[basic_task_dependency_rule]),\n    post_processing_ruleset=PostProcessingRuleset(ruleset=[basic_post_processing_rule]),\n)\n
"},{"location":"objects/","title":"Overview","text":"

Orbiter objects are returned from Rules during a translation, and are rendered to produce an Apache Airflow Project

An OrbiterProject holds everything necessary to render an Airflow Project. This is generated by a TranslationRuleset.translation_fn.

Workflows are represented by a OrbiterDAG which is a Directed Acyclic Graph (of Tasks).

OrbiterOperators represent Airflow Tasks, which are units of work. An Operator is a pre-defined task with specific functionality.

"},{"location":"objects/#orbiter.objects.OrbiterBase","title":"orbiter.objects.OrbiterBase","text":"

AbstractBaseClass for Orbiter objects, provides a number of properties

Parameters:

Name Type Description imports List[OrbiterRequirement]

List of OrbiterRequirement objects

orbiter_kwargs dict, optional

Optional dictionary of keyword arguments, to preserve what was originally parsed by a rule

orbiter_conns Set[OrbiterConnection], optional

Optional set of OrbiterConnection objects

orbiter_vars Set[OrbiterVariable], optional

Optional set of OrbiterVariable objects

orbiter_env_vars Set[OrbiterEnvVar], optional

Optional set of OrbiterEnvVar objects

orbiter_includes Set[OrbiterInclude], optional

Optional set of OrbiterInclude objects

"},{"location":"objects/dags/","title":"Workflow","text":"

Airflow workflows are represented by a DAG which is a Directed Acyclic Graph (of Tasks).

"},{"location":"objects/dags/#diagram","title":"Diagram","text":"
classDiagram\n    direction LR\n    class OrbiterRequirement[\"orbiter.objects.requirement.OrbiterRequirement\"] {\n            package: str | None\n            module: str | None\n            names: List[str] | None\n            sys_package: str | None\n    }\n    click OrbiterRequirement href \"project/#orbiter.objects.requirement.OrbiterRequirement\" \"OrbiterRequirement Documentation\"\n\n    OrbiterDAG \"via schedule\" --> OrbiterTimetable\n    OrbiterDAG --> \"many\" OrbiterOperator\n    OrbiterDAG --> \"many\" OrbiterTaskGroup\n    OrbiterDAG --> \"many\" OrbiterRequirement\n    class OrbiterDAG[\"orbiter.objects.dag.OrbiterDAG\"] {\n            imports: List[OrbiterRequirement]\n            file_path: str\n            dag_id: str\n            schedule: str | OrbiterTimetable | None\n            catchup: bool\n            start_date: DateTime\n            tags: List[str]\n            default_args: Dict[str, Any]\n            params: Dict[str, Any]\n            doc_md: str | None\n            tasks: Dict[str, OrbiterOperator]\n            orbiter_kwargs: dict\n            orbiter_conns: Set[OrbiterConnection]\n            orbiter_vars: Set[OrbiterVariable]\n            orbiter_env_vars: Set[OrbiterEnvVar]\n            orbiter_includes: Set[OrbiterInclude]\n    }\n    click OrbiterDAG href \"dags/#orbiter.objects.dag.OrbiterDAG\" \"OrbiterDAG Documentation\"\n\n    OrbiterTaskGroup --> \"many\" OrbiterRequirement\n    class OrbiterTaskGroup[\"orbiter.objects.task.OrbiterTaskGroup\"] {\n            task_group_id: str\n            tasks: List[OrbiterOperator | OrbiterTaskGroup]\n            add_downstream(str | List[str] | OrbiterTaskDependency)\n    }\n    click OrbiterTaskGroup href \"tasks#orbiter.objects.task_group.OrbiterTaskGroup\" \"OrbiterTaskGroup Documentation\"\n\n    OrbiterOperator --> \"many\" OrbiterRequirement\n    OrbiterOperator --> \"one\" OrbiterPool\n    OrbiterOperator --> \"many\" OrbiterConnection\n    OrbiterOperator --> \"many\" OrbiterVariable\n    OrbiterOperator --> \"many\" OrbiterEnvVar\n    OrbiterOperator --> \"many\" OrbiterTaskDependency\n    class OrbiterOperator[\"orbiter.objects.task.OrbiterOperator\"] {\n            imports: List[OrbiterRequirement]\n            operator: str\n            task_id: str\n            pool: str | None\n            pool_slots: int | None\n            trigger_rule: str | None\n            downstream: Set[OrbiterTaskDependency]\n            add_downstream(str | List[str] | OrbiterTaskDependency)\n    }\n    click OrbiterOperator href \"tasks#orbiter.objects.task.OrbiterOperator\" \"OrbiterOperator Documentation\"\n\n    class OrbiterTaskDependency[\"orbiter.objects.task.OrbiterTaskDependency\"] {\n            task_id: TaskId\n            downstream: TaskId | List[TaskId]\n    }\n    click OrbiterTaskDependency href \"tasks#orbiter.objects.task.OrbiterTaskDependency\" \"OrbiterTaskDependency Documentation\"\n\n    class OrbiterTimetable[\"orbiter.objects.timetables.OrbiterTimetable\"] {\n            imports: List[OrbiterRequirements]\n            orbiter_includes: Set[OrbiterIncludes]\n            **kwargs: dict\n    }\n    click OrbiterTimetable href \"project/#orbiter.objects.timetables.OrbiterTimetable\" \"OrbiterTimetable Documentation\"\n\n    class OrbiterConnection[\"orbiter.objects.connection.OrbiterConnection\"] {\n            conn_id: str\n            conn_type: str\n            **kwargs\n    }\n    click OrbiterConnection href \"project/#orbiter.objects.connection.OrbiterConnection\" \"OrbiterConnection Documentation\"\n\n    class OrbiterEnvVar[\"orbiter.objects.env_var.OrbiterEnvVar\"] {\n            key: str\n            value: str\n    }\n    click OrbiterEnvVar href \"project/#orbiter.objects.env_var.OrbiterEnvVar\" \"OrbiterEnvVar Documentation\"\n\n    class OrbiterPool[\"orbiter.objects.pool.OrbiterPool\"] {\n            name: str\n            description: str | None\n            slots: int | None\n    }\n    click OrbiterPool href \"project/#orbiter.objects.pool.OrbiterPool\" \"OrbiterPool Documentation\"\n\n    class OrbiterRequirement[\"orbiter.objects.requirement.OrbiterRequirement\"] {\n            package: str | None\n            module: str | None\n            names: List[str] | None\n            sys_package: str | None\n    }\n    click OrbiterRequirement href \"project/#orbiter.objects.requirement.OrbiterRequirement\" \"OrbiterRequirement Documentation\"\n\n    class OrbiterVariable[\"orbiter.objects.variable.OrbiterVariable\"] {\n            key: str\n            value: str\n    }\n    click OrbiterVariable href \"project/#orbiter.objects.variable.OrbiterVariable\" \"OrbiterVariable Documentation\"
"},{"location":"objects/dags/#orbiter.objects.dag.OrbiterDAG","title":"orbiter.objects.dag.OrbiterDAG","text":"

Represents an Airflow DAG, with its tasks and dependencies.

Renders to a .py file in the /dags folder

Parameters:

Name Type Description file_path str

File path of the DAG, relative to the /dags folder (filepath=my_dag.py would render to dags/my_dag.py)

dag_id str

The dag_id. Must be unique and snake_case. Good practice is to set dag_id == file_path

schedule str | OrbiterTimetable, optional

The schedule for the DAG. Defaults to None (only runs when manually triggered)

catchup bool, optional

Whether to catchup runs from the start_date to now, on first run. Defaults to False

start_date DateTime, optional

The start date for the DAG. Defaults to Unix Epoch

tags List[str], optional

Tags for the DAG, used for sorting and filtering in the Airflow UI

default_args Dict[str, Any], optional

Default arguments for any tasks in the DAG

params Dict[str, Any], optional

Params for the DAG

doc_md str, optional

Documentation for the DAG with markdown support

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/dags/#orbiter.objects.dag.OrbiterDAG.add_tasks","title":"add_tasks","text":"
add_tasks(\n    tasks: (\n        OrbiterOperator\n        | OrbiterTaskGroup\n        | Iterable[OrbiterOperator | OrbiterTaskGroup]\n    ),\n) -> \"OrbiterDAG\"\n

Add one or more OrbiterOperators to the DAG

>>> from orbiter.objects.operators.empty import OrbiterEmptyOperator\n>>> OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks(OrbiterEmptyOperator(task_id=\"bar\")).tasks\n{'bar': bar_task = EmptyOperator(task_id='bar')}\n\n>>> OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks([OrbiterEmptyOperator(task_id=\"bar\")]).tasks\n{'bar': bar_task = EmptyOperator(task_id='bar')}\n

Tip

Validation requires a OrbiterTaskGroup, OrbiterOperator (or subclass), or list of either to be passed

>>> # noinspection PyTypeChecker\n... OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks(\"bar\")\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n... OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks([\"bar\"])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description tasks OrbiterOperator | OrbiterTaskGroup | Iterable[OrbiterOperator | OrbiterTaskGroup]

List of OrbiterOperator, or OrbiterTaskGroup or subclass

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/dag.py
def add_tasks(\n    self,\n    tasks: (\n        OrbiterOperator\n        | OrbiterTaskGroup\n        | Iterable[OrbiterOperator | OrbiterTaskGroup]\n    ),\n) -> \"OrbiterDAG\":\n    \"\"\"\n    Add one or more [`OrbiterOperators`][orbiter.objects.task.OrbiterOperator] to the DAG\n\n    ```pycon\n    >>> from orbiter.objects.operators.empty import OrbiterEmptyOperator\n    >>> OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks(OrbiterEmptyOperator(task_id=\"bar\")).tasks\n    {'bar': bar_task = EmptyOperator(task_id='bar')}\n\n    >>> OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks([OrbiterEmptyOperator(task_id=\"bar\")]).tasks\n    {'bar': bar_task = EmptyOperator(task_id='bar')}\n\n    ```\n\n    !!! tip\n\n        Validation requires a `OrbiterTaskGroup`, `OrbiterOperator` (or subclass), or list of either to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks(\"bar\")\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        ... OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks([\"bar\"])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param tasks: List of [OrbiterOperator][orbiter.objects.task.OrbiterOperator], or OrbiterTaskGroup or subclass\n    :type tasks: OrbiterOperator | OrbiterTaskGroup | Iterable[OrbiterOperator | OrbiterTaskGroup]\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    if (\n        isinstance(tasks, OrbiterOperator)\n        or isinstance(tasks, OrbiterTaskGroup)\n        or issubclass(type(tasks), OrbiterOperator)\n    ):\n        tasks = [tasks]\n\n    for task in tasks:\n        try:\n            task_id = getattr(task, \"task_id\", None) or getattr(\n                task, \"task_group_id\"\n            )\n        except AttributeError:\n            raise AttributeError(\n                f\"Task {task} does not have a task_id or task_group_id attribute\"\n            )\n        self.tasks[task_id] = task\n    return self\n
"},{"location":"objects/dags/#timetables","title":"Timetables","text":""},{"location":"objects/dags/#orbiter.objects.timetables.OrbiterTimetable","title":"orbiter.objects.timetables.OrbiterTimetable","text":"

An Airflow Timetable reference.

Utilizes OrbiterInclude to add a file to a /plugins folder to register the timetable.

Parameters:

Name Type Description **kwargs

any other kwargs to provide to Timetable

"},{"location":"objects/dags/#orbiter.objects.timetables.multi_cron_timetable","title":"orbiter.objects.timetables.multi_cron_timetable","text":""},{"location":"objects/dags/#orbiter.objects.timetables.multi_cron_timetable.OrbiterMultiCronTimetable","title":"orbiter.objects.timetables.multi_cron_timetable.OrbiterMultiCronTimetable","text":"

An Airflow Timetable that can be supplied with multiple cron strings.

>>> OrbiterMultiCronTimetable(cron_defs=[\"*/5 * * * *\", \"*/7 * * * *\"])\nMultiCronTimetable(cron_defs=['*/5 * * * *', '*/7 * * * *'])\n

Parameters:

Name Type Description cron_defs List[str]

A list of cron strings

timezone str

The timezone to use for the timetable

period_length int

The length of the period

period_unit str

The unit of the period

"},{"location":"objects/project/","title":"Project","text":"

An OrbiterProject holds everything necessary to render an Airflow Project. This is generated by a TranslationRuleset.translation_fn.

"},{"location":"objects/project/#diagram","title":"Diagram","text":"
classDiagram\n    direction LR\n\n    OrbiterProject --> \"many\" OrbiterConnection\n    OrbiterProject --> \"many\" OrbiterDAG\n    OrbiterProject --> \"many\" OrbiterEnvVar\n    OrbiterProject --> \"many\" OrbiterInclude\n    OrbiterProject --> \"many\" OrbiterPool\n    OrbiterProject --> \"many\" OrbiterRequirement\n    OrbiterProject --> \"many\" OrbiterVariable\n    class OrbiterProject[\"orbiter.objects.project.OrbiterProject\"] {\n            connections: Dict[str, OrbiterConnection]\n            dags: Dict[str, OrbiterDAG]\n            env_vars: Dict[str, OrbiterEnvVar]\n            includes: Dict[str, OrbiterInclude]\n            pools: Dict[str, OrbiterPool]\n            requirements: Set[OrbiterRequirement]\n            variables: Dict[str, OrbiterVariable]\n    }\n    click OrbiterProject href \"project/#orbiter.objects.project.OrbiterProject\" \"OrbiterProject Documentation\"\n\n    class OrbiterConnection[\"orbiter.objects.connection.OrbiterConnection\"] {\n            conn_id: str\n            conn_type: str\n            **kwargs\n    }\n    click OrbiterConnection href \"project/#orbiter.objects.connection.OrbiterConnection\" \"OrbiterConnection Documentation\"\n\n    OrbiterDAG --> \"many\" OrbiterInclude\n    OrbiterDAG --> \"many\" OrbiterConnection\n    OrbiterDAG --> \"many\" OrbiterEnvVar\n    OrbiterDAG --> \"many\" OrbiterPool\n    OrbiterDAG --> \"many\" OrbiterRequirement\n    OrbiterDAG --> \"many\" OrbiterVariable\n    class OrbiterDAG[\"orbiter.objects.dag.OrbiterDAG\"] {\n            imports: List[OrbiterRequirement]\n            file_path: str\n            dag_id: str\n            schedule: str | OrbiterTimetable | None\n            catchup: bool\n            start_date: DateTime\n            tags: List[str]\n            default_args: Dict[str, Any]\n            params: Dict[str, Any]\n            doc_md: str | None\n            tasks: Dict[str, OrbiterOperator]\n            orbiter_kwargs: dict\n            orbiter_conns: Set[OrbiterConnection]\n            orbiter_vars: Set[OrbiterVariable]\n            orbiter_env_vars: Set[OrbiterEnvVar]\n            orbiter_includes: Set[OrbiterInclude]\n    }\n    click OrbiterDAG href \"dags/#orbiter.objects.dag.OrbiterDAG\" \"OrbiterDAG Documentation\"\n\n    class OrbiterEnvVar[\"orbiter.objects.env_var.OrbiterEnvVar\"] {\n            key: str\n            value: str\n    }\n    click OrbiterEnvVar href \"project/#orbiter.objects.env_var.OrbiterEnvVar\" \"OrbiterEnvVar Documentation\"\n\n    class OrbiterInclude[\"orbiter.objects.include.OrbiterInclude\"] {\n            filepath: str\n            contents: str\n    }\n    click OrbiterInclude href \"project/#orbiter.objects.include.OrbiterInclude\" \"OrbiterInclude Documentation\"\n\n    class OrbiterPool[\"orbiter.objects.pool.OrbiterPool\"] {\n            name: str\n            description: str | None\n            slots: int | None\n    }\n    click OrbiterPool href \"project/#orbiter.objects.pool.OrbiterPool\" \"OrbiterPool Documentation\"\n\n    class OrbiterRequirement[\"orbiter.objects.requirement.OrbiterRequirement\"] {\n            package: str | None\n            module: str | None\n            names: List[str] | None\n            sys_package: str | None\n    }\n    click OrbiterRequirement href \"project/#orbiter.objects.requirement.OrbiterRequirement\" \"OrbiterRequirement Documentation\"\n\n    class OrbiterVariable[\"orbiter.objects.variable.OrbiterVariable\"] {\n            key: str\n            value: str\n    }\n    click OrbiterVariable href \"project/#orbiter.objects.variable.OrbiterVariable\" \"OrbiterVariable Documentation\"\n
"},{"location":"objects/project/#orbiter.objects.connection.OrbiterConnection","title":"orbiter.objects.connection.OrbiterConnection","text":"

An Airflow Connection, rendered to an airflow_settings.yaml file.

See also other Connection documentation.

>>> OrbiterConnection(\n...     conn_id=\"my_conn_id\", conn_type=\"mysql\", host=\"localhost\", port=3306, login=\"root\"\n... ).render()\n{'conn_id': 'my_conn_id', 'conn_type': 'mysql', 'conn_host': 'localhost', 'conn_port': 3306, 'conn_login': 'root'}\n

Note

Use the utility conn_id function to generate both an OrbiterConnection and connection property for an operator

from orbiter.objects import conn_id\n\nOrbiterTask(\n    ... ,\n    **conn_id(\"my_conn_id\", conn_type=\"mysql\"),\n)\n

Parameters:

Name Type Description conn_id str

The ID of the connection

conn_type str, optional

The type of the connection, always lowercase. Defaults to 'generic'

**kwargs

Additional properties for the connection

"},{"location":"objects/project/#orbiter.objects.env_var.OrbiterEnvVar","title":"orbiter.objects.env_var.OrbiterEnvVar","text":"

Represents an Environmental Variable, renders to a line in .env file

>>> OrbiterEnvVar(key=\"foo\", value=\"bar\").render()\n'foo=bar'\n

Parameters:

Name Type Description key str

The key of the environment variable

value str

The value of the environment variable

"},{"location":"objects/project/#orbiter.objects.include.OrbiterInclude","title":"orbiter.objects.include.OrbiterInclude","text":"

Represents an included file in an /include directory

Parameters:

Name Type Description filepath str

The relative path (from the output directory) to write the file to

contents str

The contents of the file

"},{"location":"objects/project/#orbiter.objects.pool.OrbiterPool","title":"orbiter.objects.pool.OrbiterPool","text":"

An Airflow Pool, rendered to an airflow_settings.yaml file.

>>> OrbiterPool(name=\"foo\", description=\"bar\", slots=5).render()\n{'pool_name': 'foo', 'pool_description': 'bar', 'pool_slot': 5}\n

Note

Use the utility pool function to easily generate both an OrbiterPool and pool property for an operator

from orbiter.objects import pool\n\nOrbiterTask(\n    ... ,\n    **pool(\"my_pool\"),\n)\n

Parameters:

Name Type Description name str

The name of the pool

description str, optional

The description of the pool

slots int, optional

The number of slots in the pool. Defaults to 128

"},{"location":"objects/project/#orbiter.objects.requirement.OrbiterRequirement","title":"orbiter.objects.requirement.OrbiterRequirement","text":"

A requirement for a project (e.g. apache-airflow-providers-google), and it's representation in the DAG file.

Renders via the DAG File (as an import statement), requirements.txt, and packages.txt

Tip

If a given requirement has multiple packages required, it can be defined as multiple OrbiterRequirement objects.

Example:

OrbiterTask(\n    ...,\n    imports=[\n        OrbiterRequirement(package=\"apache-airflow-providers-google\", ...),\n        OrbiterRequirement(package=\"bigquery\", sys_package=\"mysql\", ...),\n    ],\n)\n

Parameters:

Name Type Description package str, optional

e.g. \"apache-airflow-providers-google\"

module str, optional

e.g. \"airflow.providers.google.cloud.operators.bigquery\", defaults to None

names List[str], optional

e.g. [\"BigQueryCreateEmptyDatasetOperator\"], defaults to []

sys_package Set[str], optional

e.g. \"mysql\" - represents a Debian system package

"},{"location":"objects/project/#orbiter.objects.variable.OrbiterVariable","title":"orbiter.objects.variable.OrbiterVariable","text":"

An Airflow Variable, rendered to an airflow_settings.yaml file.

>>> OrbiterVariable(key=\"foo\", value=\"bar\").render()\n{'variable_value': 'bar', 'variable_name': 'foo'}\n

Parameters:

Name Type Description key str

The key of the variable

value str

The value of the variable

"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject","title":"orbiter.objects.project.OrbiterProject","text":"
OrbiterProject()\n

Holds everything necessary to render an Airflow Project. This is generated by a TranslationRuleset.translation_fn.

Tip

They can be added together

>>> OrbiterProject() + OrbiterProject()\nOrbiterProject(dags=[], requirements=[], pools=[], connections=[], variables=[], env_vars=[])\n

And compared

>>> OrbiterProject() == OrbiterProject()\nTrue\n

Parameters:

Name Type Description connections Dict[str, OrbiterConnection]

A dictionary of OrbiterConnections

dags Dict[str, OrbiterDAG]

A dictionary of OrbiterDAGs

env_vars Dict[str, OrbiterEnvVar]

A dictionary of OrbiterEnvVars

includes Dict[str, OrbiterInclude]

A dictionary of OrbiterIncludes

pools Dict[str, OrbiterPool]

A dictionary of OrbiterPools

requirements Set[OrbiterRequirement]

A set of OrbiterRequirements

variables Dict[str, OrbiterVariable]

A dictionary of OrbiterVariables

Source code in orbiter/objects/project.py
def __init__(self):\n    self.dags: Dict[str, OrbiterDAG] = dict()\n    self.requirements: Set[OrbiterRequirement] = set()\n    self.pools: Dict[str, OrbiterPool] = dict()\n    self.connections: Dict[str, OrbiterConnection] = dict()\n    self.variables: Dict[str, OrbiterVariable] = dict()\n    self.env_vars: Dict[str, OrbiterEnvVar] = dict()\n    self.includes: Dict[str, OrbiterInclude] = dict()\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_connections","title":"add_connections","text":"
add_connections(\n    connections: (\n        OrbiterConnection | Iterable[OrbiterConnection]\n    ),\n) -> \"OrbiterProject\"\n

Add OrbiterConnections to the Project or override an existing connection with new properties

>>> OrbiterProject().add_connections(OrbiterConnection(conn_id='foo')).connections\n{'foo': OrbiterConnection(conn_id=foo, conn_type=generic)}\n\n>>> OrbiterProject().add_connections(\n...     [OrbiterConnection(conn_id='foo'), OrbiterConnection(conn_id='bar')]\n... ).connections\n{'foo': OrbiterConnection(conn_id=foo, conn_type=generic), 'bar': OrbiterConnection(conn_id=bar, conn_type=generic)}\n

Tip

Validation requires an OrbiterConnection to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_connections('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n>>> OrbiterProject().add_connections(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description connections OrbiterConnection | Iterable[OrbiterConnection]

List of OrbiterConnections

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_connections(\n    self, connections: OrbiterConnection | Iterable[OrbiterConnection]\n) -> \"OrbiterProject\":\n    \"\"\"Add [`OrbiterConnections`][orbiter.objects.connection.OrbiterConnection] to the Project\n    or override an existing connection with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_connections(OrbiterConnection(conn_id='foo')).connections\n    {'foo': OrbiterConnection(conn_id=foo, conn_type=generic)}\n\n    >>> OrbiterProject().add_connections(\n    ...     [OrbiterConnection(conn_id='foo'), OrbiterConnection(conn_id='bar')]\n    ... ).connections\n    {'foo': OrbiterConnection(conn_id=foo, conn_type=generic), 'bar': OrbiterConnection(conn_id=bar, conn_type=generic)}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterConnection` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_connections('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        >>> OrbiterProject().add_connections(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n\n    :param connections: List of [`OrbiterConnections`][orbiter.objects.connection.OrbiterConnection]\n    :type connections: List[OrbiterConnection] | OrbiterConnection\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"  # noqa: E501\n    for connection in (\n        [connections] if isinstance(connections, OrbiterConnection) else connections\n    ):\n        self.connections[connection.conn_id] = connection\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_dags","title":"add_dags","text":"
add_dags(\n    dags: OrbiterDAG | Iterable[OrbiterDAG],\n) -> \"OrbiterProject\"\n

Add OrbiterDAGs (and any OrbiterRequirements, OrbiterConns, OrbiterVars, OrbiterPools, OrbiterEnvVars, etc.) to the Project.

>>> OrbiterProject().add_dags(OrbiterDAG(dag_id='foo', file_path=\"\")).dags['foo'].repr()\n'OrbiterDAG(dag_id=foo, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)'\n\n>>> dags = OrbiterProject().add_dags(\n...     [OrbiterDAG(dag_id='foo', file_path=\"\"), OrbiterDAG(dag_id='bar', file_path=\"\")]\n... ).dags; dags['foo'].repr(), dags['bar'].repr()\n... # doctest: +NORMALIZE_WHITESPACE\n('OrbiterDAG(dag_id=foo, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)', 'OrbiterDAG(dag_id=bar, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)')\n\n>>> # An example adding a little of everything, including deeply nested things\n... from orbiter.objects.operators.bash import OrbiterBashOperator\n>>> from orbiter.objects.timetables.multi_cron_timetable import OrbiterMultiCronTimetable\n>>> from orbiter.objects.callbacks.smtp import OrbiterSmtpNotifierCallback\n>>> OrbiterProject().add_dags(OrbiterDAG(\n...     dag_id='foo', file_path=\"\",\n...     orbiter_env_vars={OrbiterEnvVar(key=\"foo\", value=\"bar\")},\n...     orbiter_includes={OrbiterInclude(filepath='foo.txt', contents=\"Hello, World!\")},\n...     schedule=OrbiterMultiCronTimetable(cron_defs=[\"0 */5 * * *\", \"0 */3 * * *\"]),\n...     tasks={'foo': OrbiterTaskGroup(task_group_id=\"foo\",\n...         tasks=[OrbiterBashOperator(\n...             task_id='foo', bash_command='echo \"Hello, World!\"',\n...             orbiter_pool=OrbiterPool(name='foo', slots=1),\n...             orbiter_vars={OrbiterVariable(key='foo', value='bar')},\n...             orbiter_conns={OrbiterConnection(conn_id='foo')},\n...             orbiter_env_vars={OrbiterEnvVar(key='foo', value='bar')},\n...             on_success_callback=OrbiterSmtpNotifierCallback(\n...                 to=\"foo@bar.com\",\n...                 smtp_conn_id=\"SMTP\",\n...                 orbiter_conns={OrbiterConnection(conn_id=\"SMTP\", conn_type=\"smtp\")}\n...             )\n...         )]\n...     )}\n... ))\n... # doctest: +NORMALIZE_WHITESPACE\nOrbiterProject(dags=[foo],\nrequirements=[OrbiterRequirements(names=[DAG], package=apache-airflow, module=airflow, sys_package=None),\nOrbiterRequirements(names=[BashOperator], package=apache-airflow, module=airflow.operators.bash, sys_package=None),\nOrbiterRequirements(names=[send_smtp_notification], package=apache-airflow-providers-smtp, module=airflow.providers.smtp.notifications.smtp, sys_package=None),\nOrbiterRequirements(names=[TaskGroup], package=apache-airflow, module=airflow.utils.task_group, sys_package=None),\nOrbiterRequirements(names=[MultiCronTimetable], package=croniter, module=multi_cron_timetable, sys_package=None),\nOrbiterRequirements(names=[DateTime,Timezone], package=pendulum, module=pendulum, sys_package=None)],\npools=['foo'],\nconnections=['SMTP', 'foo'],\nvariables=['foo'],\nenv_vars=['foo'])\n

Tip

Validation requires an OrbiterDAG to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_dags('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n>>> OrbiterProject().add_dags(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description dags OrbiterDAG | Iterable[OrbiterDAG]

List of OrbiterDAGs

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_dags(self, dags: OrbiterDAG | Iterable[OrbiterDAG]) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterDAGs][orbiter.objects.dag.OrbiterDAG]\n    (and any [OrbiterRequirements][orbiter.objects.requirement.OrbiterRequirement],\n    [OrbiterConns][orbiter.objects.connection.OrbiterConnection],\n    [OrbiterVars][orbiter.objects.variable.OrbiterVariable],\n    [OrbiterPools][orbiter.objects.pool.OrbiterPool],\n    [OrbiterEnvVars][orbiter.objects.env_var.OrbiterEnvVar], etc.)\n    to the Project.\n\n    ```pycon\n    >>> OrbiterProject().add_dags(OrbiterDAG(dag_id='foo', file_path=\"\")).dags['foo'].repr()\n    'OrbiterDAG(dag_id=foo, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)'\n\n    >>> dags = OrbiterProject().add_dags(\n    ...     [OrbiterDAG(dag_id='foo', file_path=\"\"), OrbiterDAG(dag_id='bar', file_path=\"\")]\n    ... ).dags; dags['foo'].repr(), dags['bar'].repr()\n    ... # doctest: +NORMALIZE_WHITESPACE\n    ('OrbiterDAG(dag_id=foo, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)', 'OrbiterDAG(dag_id=bar, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)')\n\n    >>> # An example adding a little of everything, including deeply nested things\n    ... from orbiter.objects.operators.bash import OrbiterBashOperator\n    >>> from orbiter.objects.timetables.multi_cron_timetable import OrbiterMultiCronTimetable\n    >>> from orbiter.objects.callbacks.smtp import OrbiterSmtpNotifierCallback\n    >>> OrbiterProject().add_dags(OrbiterDAG(\n    ...     dag_id='foo', file_path=\"\",\n    ...     orbiter_env_vars={OrbiterEnvVar(key=\"foo\", value=\"bar\")},\n    ...     orbiter_includes={OrbiterInclude(filepath='foo.txt', contents=\"Hello, World!\")},\n    ...     schedule=OrbiterMultiCronTimetable(cron_defs=[\"0 */5 * * *\", \"0 */3 * * *\"]),\n    ...     tasks={'foo': OrbiterTaskGroup(task_group_id=\"foo\",\n    ...         tasks=[OrbiterBashOperator(\n    ...             task_id='foo', bash_command='echo \"Hello, World!\"',\n    ...             orbiter_pool=OrbiterPool(name='foo', slots=1),\n    ...             orbiter_vars={OrbiterVariable(key='foo', value='bar')},\n    ...             orbiter_conns={OrbiterConnection(conn_id='foo')},\n    ...             orbiter_env_vars={OrbiterEnvVar(key='foo', value='bar')},\n    ...             on_success_callback=OrbiterSmtpNotifierCallback(\n    ...                 to=\"foo@bar.com\",\n    ...                 smtp_conn_id=\"SMTP\",\n    ...                 orbiter_conns={OrbiterConnection(conn_id=\"SMTP\", conn_type=\"smtp\")}\n    ...             )\n    ...         )]\n    ...     )}\n    ... ))\n    ... # doctest: +NORMALIZE_WHITESPACE\n    OrbiterProject(dags=[foo],\n    requirements=[OrbiterRequirements(names=[DAG], package=apache-airflow, module=airflow, sys_package=None),\n    OrbiterRequirements(names=[BashOperator], package=apache-airflow, module=airflow.operators.bash, sys_package=None),\n    OrbiterRequirements(names=[send_smtp_notification], package=apache-airflow-providers-smtp, module=airflow.providers.smtp.notifications.smtp, sys_package=None),\n    OrbiterRequirements(names=[TaskGroup], package=apache-airflow, module=airflow.utils.task_group, sys_package=None),\n    OrbiterRequirements(names=[MultiCronTimetable], package=croniter, module=multi_cron_timetable, sys_package=None),\n    OrbiterRequirements(names=[DateTime,Timezone], package=pendulum, module=pendulum, sys_package=None)],\n    pools=['foo'],\n    connections=['SMTP', 'foo'],\n    variables=['foo'],\n    env_vars=['foo'])\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterDAG` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_dags('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        >>> OrbiterProject().add_dags(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n\n    :param dags: List of [OrbiterDAGs][orbiter.objects.dag.OrbiterDAG]\n    :type dags: List[OrbiterDAG] | OrbiterDAG\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"  # noqa: E501\n\n    # noinspection t\n    def _add_recursively(\n        things: Iterable[\n            OrbiterOperator | OrbiterTaskGroup | OrbiterCallback | OrbiterTimetable\n        ],\n    ):\n        for thing in things:\n            if isinstance(thing, str):\n                continue\n            if hasattr(thing, \"orbiter_pool\") and (pool := thing.orbiter_pool):\n                self.add_pools(pool)\n            if hasattr(thing, \"orbiter_conns\") and (conns := thing.orbiter_conns):\n                self.add_connections(conns)\n            if hasattr(thing, \"orbiter_vars\") and (variables := thing.orbiter_vars):\n                self.add_variables(variables)\n            if hasattr(thing, \"orbiter_env_vars\") and (\n                env_vars := thing.orbiter_env_vars\n            ):\n                self.add_env_vars(env_vars)\n            if hasattr(thing, \"orbiter_includes\") and (\n                includes := thing.orbiter_includes\n            ):\n                self.add_includes(includes)\n            if hasattr(thing, \"imports\") and (imports := thing.imports):\n                self.add_requirements(imports)\n            if isinstance(thing, OrbiterTaskGroup) and (tasks := thing.tasks):\n                _add_recursively(tasks)\n\n            # find callbacks in any 'model extra' or attributes named\n            # \"on_success_callback\" or \"on_failure_callback\"\n            if (\n                hasattr(thing, \"__dict__\")\n                and hasattr(thing, \"model_extra\")\n                and len(\n                    (\n                        callbacks := {\n                            k: v\n                            for k, v in (\n                                (thing.__dict__ or dict())\n                                | (thing.model_extra or dict())\n                            ).items()\n                            if k in (\"on_success_callback\", \"on_failure_callback\")\n                            and issubclass(type(v), OrbiterCallback)\n                        }\n                    )\n                )\n            ):\n                _add_recursively(callbacks.values())\n\n    for dag in [dags] if isinstance(dags, OrbiterDAG) else dags:\n        dag_id = dag.dag_id\n\n        # Add or update the DAG\n        if dag_id in self.dags:\n            self.dags[dag_id] += dag\n        else:\n            self.dags[dag_id] = dag\n\n        # Add imports to the project\n        self.add_requirements(dag.imports)\n\n        # Add anything that might be in the tasks of the DAG - such as imports, Connections, etc\n        _add_recursively((dag.tasks or {}).values())\n\n        # Add anything that might be in the `dag.schedule` - such as Includes, Timetables, Connections, etc\n        _add_recursively([dag.schedule])\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_env_vars","title":"add_env_vars","text":"
add_env_vars(\n    env_vars: OrbiterEnvVar | Iterable[OrbiterEnvVar],\n) -> \"OrbiterProject\"\n

Add OrbiterEnvVars to the Project or override an existing env var with new properties

>>> OrbiterProject().add_env_vars(OrbiterEnvVar(key=\"foo\", value=\"bar\")).env_vars\n{'foo': OrbiterEnvVar(key='foo', value='bar')}\n\n>>> OrbiterProject().add_env_vars([OrbiterEnvVar(key=\"foo\", value=\"bar\")]).env_vars\n{'foo': OrbiterEnvVar(key='foo', value='bar')}\n

Tip

Validation requires an OrbiterEnvVar to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_env_vars('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n\n>>> # noinspection PyTypeChecker\n... OrbiterProject().add_env_vars(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description env_vars OrbiterEnvVar | Iterable[OrbiterEnvVar]

List of OrbiterEnvVar

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_env_vars(\n    self, env_vars: OrbiterEnvVar | Iterable[OrbiterEnvVar]\n) -> \"OrbiterProject\":\n    \"\"\"\n    Add [OrbiterEnvVars][orbiter.objects.env_var.OrbiterEnvVar] to the Project\n    or override an existing env var with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_env_vars(OrbiterEnvVar(key=\"foo\", value=\"bar\")).env_vars\n    {'foo': OrbiterEnvVar(key='foo', value='bar')}\n\n    >>> OrbiterProject().add_env_vars([OrbiterEnvVar(key=\"foo\", value=\"bar\")]).env_vars\n    {'foo': OrbiterEnvVar(key='foo', value='bar')}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterEnvVar` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_env_vars('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_env_vars(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n\n    :param env_vars: List of [OrbiterEnvVar][orbiter.objects.env_var.OrbiterEnvVar]\n    :type env_vars: List[OrbiterEnvVar] | OrbiterEnvVar\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for env_var in [env_vars] if isinstance(env_vars, OrbiterEnvVar) else env_vars:\n        self.env_vars[env_var.key] = env_var\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_includes","title":"add_includes","text":"
add_includes(\n    includes: OrbiterInclude | Iterable[OrbiterInclude],\n) -> \"OrbiterProject\"\n

Add OrbiterIncludes to the Project or override an existing OrbiterInclude with new properties

>>> OrbiterProject().add_includes(OrbiterInclude(filepath=\"foo\", contents=\"bar\")).includes\n{'foo': OrbiterInclude(filepath='foo', contents='bar')}\n\n>>> OrbiterProject().add_includes([OrbiterInclude(filepath=\"foo\", contents=\"bar\")]).includes\n{'foo': OrbiterInclude(filepath='foo', contents='bar')}\n

Tip

Validation requires an OrbiterInclude to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_includes('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n\n>>> # noinspection PyTypeChecker\n... OrbiterProject().add_includes(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description includes OrbiterInclude | Iterable[OrbiterInclude]

List of OrbiterIncludes

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_includes(\n    self, includes: OrbiterInclude | Iterable[OrbiterInclude]\n) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterIncludes][orbiter.objects.include.OrbiterInclude] to the Project\n    or override an existing [OrbiterInclude][orbiter.objects.include.OrbiterInclude] with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_includes(OrbiterInclude(filepath=\"foo\", contents=\"bar\")).includes\n    {'foo': OrbiterInclude(filepath='foo', contents='bar')}\n\n    >>> OrbiterProject().add_includes([OrbiterInclude(filepath=\"foo\", contents=\"bar\")]).includes\n    {'foo': OrbiterInclude(filepath='foo', contents='bar')}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterInclude` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_includes('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_includes(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param includes: List of [OrbiterIncludes][orbiter.objects.include.OrbiterInclude]\n    :type includes: List[OrbiterInclude]\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for include in [includes] if isinstance(includes, OrbiterInclude) else includes:\n        self.includes[include.filepath] = include\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_pools","title":"add_pools","text":"
add_pools(\n    pools: OrbiterPool | Iterable[OrbiterPool],\n) -> \"OrbiterProject\"\n

Add OrbiterPool to the Project or override existing pools with new properties

>>> OrbiterProject().add_pools(OrbiterPool(name=\"foo\", slots=1)).pools\n{'foo': OrbiterPool(name='foo', description='', slots=1)}\n\n>>> ( OrbiterProject()\n...     .add_pools([OrbiterPool(name=\"foo\", slots=1)])\n...     .add_pools([OrbiterPool(name=\"foo\", slots=2)])\n...     .pools\n... )\n{'foo': OrbiterPool(name='foo', description='', slots=2)}\n

Tip

Validation requires an OrbiterPool to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_pools('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n... OrbiterProject().add_pools(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description pools OrbiterPool | Iterable[OrbiterPool]

List of OrbiterPools

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_pools(self, pools: OrbiterPool | Iterable[OrbiterPool]) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterPool][orbiter.objects.pool.OrbiterPool] to the Project\n    or override existing pools with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_pools(OrbiterPool(name=\"foo\", slots=1)).pools\n    {'foo': OrbiterPool(name='foo', description='', slots=1)}\n\n    >>> ( OrbiterProject()\n    ...     .add_pools([OrbiterPool(name=\"foo\", slots=1)])\n    ...     .add_pools([OrbiterPool(name=\"foo\", slots=2)])\n    ...     .pools\n    ... )\n    {'foo': OrbiterPool(name='foo', description='', slots=2)}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterPool` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_pools('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_pools(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param pools: List of [OrbiterPools][orbiter.objects.pool.OrbiterPool]\n    :type pools: List[OrbiterPool] | OrbiterPool\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for pool in [pools] if isinstance(pools, OrbiterPool) else pools:\n        if pool.name in self.pools:\n            self.pools[pool.name] += pool\n        else:\n            self.pools[pool.name] = pool\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_requirements","title":"add_requirements","text":"
add_requirements(\n    requirements: (\n        OrbiterRequirement | Iterable[OrbiterRequirement]\n    ),\n) -> \"OrbiterProject\"\n

Add OrbiterRequirements to the Project or override an existing requirement with new properties

>>> OrbiterProject().add_requirements(\n...    OrbiterRequirement(package=\"apache-airflow\", names=['foo'], module='bar'),\n... ).requirements\n{OrbiterRequirements(names=[foo], package=apache-airflow, module=bar, sys_package=None)}\n\n>>> OrbiterProject().add_requirements(\n...    [OrbiterRequirement(package=\"apache-airflow\", names=['foo'], module='bar')],\n... ).requirements\n{OrbiterRequirements(names=[foo], package=apache-airflow, module=bar, sys_package=None)}\n

Tip

Validation requires an OrbiterRequirement to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_requirements('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n>>> OrbiterProject().add_requirements(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description requirements OrbiterRequirement | Iterable[OrbiterRequirement]

List of OrbiterRequirements

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_requirements(\n    self, requirements: OrbiterRequirement | Iterable[OrbiterRequirement]\n) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterRequirements][orbiter.objects.requirement.OrbiterRequirement] to the Project\n    or override an existing requirement with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_requirements(\n    ...    OrbiterRequirement(package=\"apache-airflow\", names=['foo'], module='bar'),\n    ... ).requirements\n    {OrbiterRequirements(names=[foo], package=apache-airflow, module=bar, sys_package=None)}\n\n    >>> OrbiterProject().add_requirements(\n    ...    [OrbiterRequirement(package=\"apache-airflow\", names=['foo'], module='bar')],\n    ... ).requirements\n    {OrbiterRequirements(names=[foo], package=apache-airflow, module=bar, sys_package=None)}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterRequirement` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_requirements('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        >>> OrbiterProject().add_requirements(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param requirements: List of [OrbiterRequirements][orbiter.objects.requirement.OrbiterRequirement]\n    :type requirements: List[OrbiterRequirement] | OrbiterRequirement\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for requirement in (\n        [requirements]\n        if isinstance(requirements, OrbiterRequirement)\n        else requirements\n    ):\n        self.requirements.add(requirement)\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_variables","title":"add_variables","text":"
add_variables(\n    variables: OrbiterVariable | Iterable[OrbiterVariable],\n) -> \"OrbiterProject\"\n

Add OrbiterVariables to the Project or override an existing variable with new properties

>>> OrbiterProject().add_variables(OrbiterVariable(key=\"foo\", value=\"bar\")).variables\n{'foo': OrbiterVariable(key='foo', value='bar')}\n\n>>> OrbiterProject().add_variables([OrbiterVariable(key=\"foo\", value=\"bar\")]).variables\n{'foo': OrbiterVariable(key='foo', value='bar')}\n

Tip

Validation requires an OrbiterVariable to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_variables('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n... OrbiterProject().add_variables(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description variables OrbiterVariable | Iterable[OrbiterVariable]

List of OrbiterVariable

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_variables(\n    self, variables: OrbiterVariable | Iterable[OrbiterVariable]\n) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterVariables][orbiter.objects.variable.OrbiterVariable] to the Project\n    or override an existing variable with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_variables(OrbiterVariable(key=\"foo\", value=\"bar\")).variables\n    {'foo': OrbiterVariable(key='foo', value='bar')}\n\n    >>> OrbiterProject().add_variables([OrbiterVariable(key=\"foo\", value=\"bar\")]).variables\n    {'foo': OrbiterVariable(key='foo', value='bar')}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterVariable` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_variables('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_variables(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param variables: List of [OrbiterVariable][orbiter.objects.variable.OrbiterVariable]\n    :type variables: List[OrbiterVariable] | OrbiterVariable\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for variable in (\n        [variables] if isinstance(variables, OrbiterVariable) else variables\n    ):\n        self.variables[variable.key] = variable\n    return self\n
"},{"location":"objects/Tasks/","title":"Overview","text":"

Airflow Tasks are units of work. An Operator is a pre-defined task with specific functionality.

Operators can be looked up in the Astronomer Registry.

The easiest way to utilize an operator is to use a subclass of OrbiterOperator (e.g. OrbiterBashOperator).

If an OrbiterOperator subclass doesn't exist for your use case, you can:

1) Utilize OrbiterTask

from orbiter.objects.requirement import OrbiterRequirement\nfrom orbiter.objects.task import OrbiterTask\nfrom orbiter.rules import task_rule\n\n@task_rule\ndef my_rule(val: dict):\n    return OrbiterTask(\n        task_id=\"my_task\",\n        imports=[OrbiterRequirement(\n            package=\"apache-airflow\",\n            module=\"airflow.operators.trigger_dagrun\",\n            names=[\"TriggerDagRunOperator\"],\n        )],\n        ...\n    )\n

2) Create a new subclass of OrbiterOperator (beneficial if you are using it frequently in separate @task_rules)

from orbiter.objects.task import OrbiterOperator\nfrom orbiter.objects.requirement import OrbiterRequirement\nfrom orbiter.rules import task_rule\n\nclass OrbiterTriggerDagRunOperator(OrbiterOperator):\n    # Define the imports required for the operator, and the operator name\n    imports = [\n        OrbiterRequirement(\n            package=\"apache-airflow\",\n            module=\"airflow.operators.trigger_dagrun\",\n            names=[\"TriggerDagRunOperator\"],\n        )\n    ]\n    operator: str = \"PythonOperator\"\n\n    # Add fields should be rendered in the output\n    render_attributes = OrbiterOperator.render_attributes + [\n        ...\n    ]\n\n    # Add the fields that are required for the operator here, with their types\n    # Not all Airflow Operator fields are required, just the ones you will use.\n    trigger_dag_id: str\n    ...\n\n@task_rule\ndef my_rule(val: dict):\n    return OrbiterTriggerDagRunOperator(...)\n

"},{"location":"objects/Tasks/#diagram","title":"Diagram","text":"
classDiagram\n    direction LR\n    OrbiterOperator \"implements\" <|-- OrbiterTask\n    OrbiterOperator --> \"many\" OrbiterCallback\n    class OrbiterOperator[\"orbiter.objects.task.OrbiterOperator\"] {\n            imports: List[OrbiterRequirement]\n            operator: str\n            task_id: str\n            pool: str | None\n            pool_slots: int | None\n            trigger_rule: str | None\n            downstream: Set[OrbiterTaskDependency]\n            add_downstream(str | List[str] | OrbiterTaskDependency)\n    }\n    click OrbiterOperator href \"tasks#orbiter.objects.task.OrbiterOperator\" \"OrbiterOperator Documentation\"\n\n    class OrbiterTask[\"orbiter.objects.task.OrbiterTask\"] {\n        <<OrbiterOperator>>\n            <<OrbiterOperator>>\n            imports: List[OrbiterRequirement]\n            task_id: str\n            **kwargs\n    }\n    click OrbiterTask href \"tasks#orbiter.objects.task.OrbiterTask\" \"OrbiterTask Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterBashOperator\n    class OrbiterBashOperator[\"orbiter.objects.operators.bash.OrbiterBashOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"BashOperator\"\n            task_id: str\n            bash_command: str\n    }\n    click OrbiterBashOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.bash.OrbiterBashOperator\" \"OrbiterBashOperator Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterEmailOperator\n    class OrbiterEmailOperator[\"orbiter.objects.operators.smtp.OrbiterEmailOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"EmailOperator\"\n            task_id: str\n            to: str | list[str]\n            subject: str\n            html_content: str\n            files: list | None\n            conn_id: str\n    }\n    click OrbiterEmailOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.smtp.OrbiterEmailOperator\" \"OrbiterEmailOperator Documentation\"\n\n  OrbiterOperator  \"implements\" <|--  OrbiterEmptyOperator\n    class OrbiterEmptyOperator[\"orbiter.objects.operators.empty.OrbiterEmptyOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"BashOperator\"\n            task_id: str\n    }\n    click OrbiterEmptyOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.empty.OrbiterEmptyOperator\" \"OrbiterEmptyOperator Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterPythonOperator\n    class OrbiterPythonOperator[\"orbiter.objects.operators.python.OrbiterPythonOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"PythonOperator\"\n            task_id: str\n            python_callable: Callable\n            op_args: list | None\n            op_kwargs: dict | None\n    }\n    click OrbiterPythonOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.python.OrbiterPythonOperator\" \"OrbiterPythonOperator Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterSQLExecuteQueryOperator\n    class OrbiterSQLExecuteQueryOperator[\"orbiter.objects.operators.sql.OrbiterSQLExecuteQueryOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"SQLExecuteQueryOperator\"\n            task_id: str\n            conn_id: str\n            sql: str\n    }\n    click OrbiterSQLExecuteQueryOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.sql.OrbiterSQLExecuteQueryOperator\" \"OrbiterSQLExecuteQueryOperator Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterSSHOperator\n    class OrbiterSSHOperator[\"orbiter.objects.operators.ssh.OrbiterSSHOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"SSHOperator\"\n            task_id: str\n            ssh_conn_id: str\n            command: str\n            environment: Dict[str, str] | None\n    }\n    click OrbiterSSHOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.ssh.OrbiterSSHOperator\" \"OrbiterSSHOperator Documentation\"\n\n    class OrbiterCallback[\"orbiter.objects.callbacks.OrbiterCallback\"] {\n            function: str\n    }\n    click OrbiterCallback href \"Operators_and_Callbacks/callbacks#orbiter.objects.callbacks.OrbiterCallback\" \"OrbiterCallback Documentation\"\n\n    OrbiterCallback \"implements\" <|--  OrbiterSmtpNotifierCallback\n    class OrbiterSmtpNotifierCallback[\"orbiter.objects.callbacks.smtp.OrbiterSmtpNotifierCallback\"] {\n        <<OrbiterCallback>>\n            to: str\n            from_email: str\n            smtp_conn_id: str\n            subject: str\n            html_content: str\n            cc: str | Iterable[str]\n    }\n    click OrbiterSmtpNotifierCallback href \"Operators_and_Callbacks/callbacks#orbiter.objects.callbacks.smtp.OrbiterSmtpNotifierCallback\" \"OrbiterSmtpNotifierCallback Documentation\"
"},{"location":"objects/Tasks/#orbiter.objects.task.OrbiterOperator","title":"orbiter.objects.task.OrbiterOperator","text":"

Abstract class representing a Task in Airflow, must be subclassed (such as OrbiterBashOperator)

Instantiation/inheriting:

>>> from orbiter.objects import OrbiterRequirement\n>>> class OrbiterMyOperator(OrbiterOperator):\n...   imports: ImportList = [OrbiterRequirement(package=\"apache-airflow\")]\n...   operator: str = \"MyOperator\"\n\n>>> foo = OrbiterMyOperator(task_id=\"task_id\"); foo\ntask_id_task = MyOperator(task_id='task_id')\n

Adding single downstream tasks:

>>> foo.add_downstream(\"downstream\").downstream\n{task_id_task >> downstream_task}\n

Adding multiple downstream tasks:

>>> sorted(list(foo.add_downstream([\"a\", \"b\"]).downstream))\n[task_id_task >> [a_task, b_task], task_id_task >> downstream_task]\n

Note

Validation - task_id in OrbiterTaskDependency must match this task_id

>>> foo.add_downstream(OrbiterTaskDependency(task_id=\"other\", downstream=\"bar\")).downstream\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\nValueError: Task dependency ... has a different task_id than task_id\n

Parameters:

Name Type Description imports List[OrbiterRequirement]

List of requirements for the operator

task_id str

The task_id for the operator, must be unique and snake_case

trigger_rule str, optional

Conditions under which to start the task (docs)

pool str, optional

Name of the pool to use

pool_slots int, optional

Slots for this task to occupy

operator str, optional

Operator name

downstream Set[OrbiterTaskDependency], optional

Downstream tasks, defaults to set()

**kwargs

Other properties that may be passed to operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/#orbiter.objects.task.OrbiterTaskDependency","title":"orbiter.objects.task.OrbiterTaskDependency","text":"

Represents a task dependency, which is added to either an OrbiterOperator or an OrbiterTaskGroup.

Can take a single downstream task_id

>>> OrbiterTaskDependency(task_id=\"task_id\", downstream=\"downstream\")\ntask_id_task >> downstream_task\n

or a list of downstream task_ids

>>> OrbiterTaskDependency(task_id=\"task_id\", downstream=[\"a\", \"b\"])\ntask_id_task >> [a_task, b_task]\n

Parameters:

Name Type Description task_id str

The task_id for the operator

downstream str | List[str]

downstream tasks

"},{"location":"objects/Tasks/#orbiter.objects.task.OrbiterTask","title":"orbiter.objects.task.OrbiterTask","text":"

A generic Airflow OrbiterOperator that can be instantiated directly.

The operator that is instantiated is inferred from the imports field.

The first *Operator or *Sensor import is used.

View info for specific operators at the Astronomer Registry.

>>> from orbiter.objects.requirement import OrbiterRequirement\n>>> OrbiterTask(task_id=\"foo\", bash_command=\"echo 'hello world'\", other=1, imports=[\n...   OrbiterRequirement(package=\"apache-airflow\", module=\"airflow.operators.bash\", names=[\"BashOperator\"])\n... ])\nfoo_task = BashOperator(task_id='foo', bash_command=\"echo 'hello world'\", other=1)\n\n>>> def foo():\n...   pass\n>>> OrbiterTask(task_id=\"foo\", python_callable=foo, other=1, imports=[\n...   OrbiterRequirement(package=\"apache-airflow\", module=\"airflow.sensors.python\", names=[\"PythonSensor\"])\n... ])\ndef foo():\n    pass\nfoo_task = PythonSensor(task_id='foo', other=1, python_callable=foo)\n

Parameters:

Name Type Description task_id str

The task_id for the operator. Must be unique and snake_case

imports List[OrbiterRequirement]

List of requirements for the operator. The Operator is inferred from first *Operator or *Sensor imported.

**kwargs

Any other keyword arguments to be passed to the operator

"},{"location":"objects/Tasks/#orbiter.objects.task_group.OrbiterTaskGroup","title":"orbiter.objects.task_group.OrbiterTaskGroup","text":"

Represents a TaskGroup in Airflow, which contains multiple tasks

>>> from orbiter.objects.operators.bash import OrbiterBashOperator\n>>> OrbiterTaskGroup(task_group_id=\"foo\", tasks=[\n...   OrbiterBashOperator(task_id=\"b\", bash_command=\"b\"),\n...   OrbiterBashOperator(task_id=\"a\", bash_command=\"a\").add_downstream(\"b\"),\n... ])\nwith TaskGroup(group_id='foo') as foo:\n    b_task = BashOperator(task_id='b', bash_command='b')\n    a_task = BashOperator(task_id='a', bash_command='a')\n    a_task >> b_task\n

Parameters:

Name Type Description task_group_id str

The id of the TaskGroup

tasks List[OrbiterOperator | OrbiterTaskGroup]

The tasks in the TaskGroup

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/callbacks/","title":"Callbacks","text":"

Airflow callback functions are often used to send emails, slack messages, or other notifications when a task fails, succeeds, or is retried. They can also run any general Python function.

"},{"location":"objects/Tasks/Operators_and_Callbacks/callbacks/#orbiter.objects.callbacks.OrbiterCallback","title":"orbiter.objects.callbacks.OrbiterCallback","text":"

Abstract class representing an Airflow callback function, which might be used in DAG.on_failure_callback, or Task.on_success_callback, or etc.

>>> class OrbiterMyCallback(OrbiterCallback):\n...   function: str = \"my_callback\"\n...   foo: str\n...   bar: str\n...   render_attributes: RenderAttributes = [\"foo\", \"bar\"]\n>>> OrbiterMyCallback(foo=\"fop\", bar=\"bop\")\nmy_callback(foo='fop', bar='bop')\n

Parameters:

Name Type Description function str

The name of the function to call

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/callbacks/#orbiter.objects.callbacks.smtp","title":"orbiter.objects.callbacks.smtp","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/callbacks/#orbiter.objects.callbacks.smtp.OrbiterSmtpNotifierCallback","title":"orbiter.objects.callbacks.smtp.OrbiterSmtpNotifierCallback","text":"

An Airflow SMTP Callback (link)

Note

Use smtp_conn_id and reference an SMTP Connection.

You can use the **conn_id(\"SMTP\", conn_type=\"smtp\") utility function to set both properties at once.

>>> [_import] = OrbiterSmtpNotifierCallback(to=\"foo@test.com\").imports; _import\nOrbiterRequirements(names=[send_smtp_notification], package=apache-airflow-providers-smtp, module=airflow.providers.smtp.notifications.smtp, sys_package=None)\n\n>>> OrbiterSmtpNotifierCallback(to=\"foo@test.com\", from_email=\"bar@test.com\", subject=\"Hello\", html_content=\"World\")\nsend_smtp_notification(to='foo@test.com', from_email='bar@test.com', smtp_conn_id='SMTP', subject='Hello', html_content='World')\n

Parameters:

Name Type Description to str | Iterable[str]

The email address to send to

from_email str, optional

The email address to send from

smtp_conn_id str, optional

The connection id to use (Note: use the **conn_id(...) utility function). Defaults to \"SMTP\"

subject str, optional

The subject of the email

html_content str, optional

The content of the email

cc str | Iterable[str], optional

The email address to cc

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/","title":"Operators","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.bash","title":"orbiter.objects.operators.bash","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.bash.OrbiterBashOperator","title":"orbiter.objects.operators.bash.OrbiterBashOperator","text":"

Bases: OrbiterOperator

An Airflow BashOperator. Used to run shell commands.

>>> OrbiterBashOperator(task_id=\"foo\", bash_command=\"echo 'hello world'\")\nfoo_task = BashOperator(task_id='foo', bash_command=\"echo 'hello world'\")\n

Parameters:

Name Type Description task_id str

The task_id for the operator

bash_command str

The shell command to execute

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.empty","title":"orbiter.objects.operators.empty","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.empty.OrbiterEmptyOperator","title":"orbiter.objects.operators.empty.OrbiterEmptyOperator","text":"

Bases: OrbiterOperator

An Airflow EmptyOperator. Does nothing.

>>> OrbiterEmptyOperator(task_id=\"foo\")\nfoo_task = EmptyOperator(task_id='foo')\n

Parameters:

Name Type Description task_id str

The task_id for the operator

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.python","title":"orbiter.objects.operators.python","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.python.OrbiterPythonOperator","title":"orbiter.objects.operators.python.OrbiterPythonOperator","text":"

Bases: OrbiterOperator

An Airflow PythonOperator. Used to execute any Python Function.

>>> def foo(a, b):\n...    print(a + b)\n>>> OrbiterPythonOperator(task_id=\"foo\", python_callable=foo)\ndef foo(a, b):\n   print(a + b)\nfoo_task = PythonOperator(task_id='foo', python_callable=foo)\n

Parameters:

Name Type Description task_id str

The task_id for the operator

python_callable Callable

The python function to execute

op_args list | None, optional

The arguments to pass to the python function, defaults to None

op_kwargs dict | None, optional

The keyword arguments to pass to the python function, defaults to None

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.smtp","title":"orbiter.objects.operators.smtp","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.smtp.OrbiterEmailOperator","title":"orbiter.objects.operators.smtp.OrbiterEmailOperator","text":"

Bases: OrbiterOperator

An Airflow EmailOperator. Used to send emails.

>>> OrbiterEmailOperator(\n...   task_id=\"foo\", to=\"humans@astronomer.io\", subject=\"Hello\", html_content=\"World!\"\n... )\nfoo_task = EmailOperator(task_id='foo', to='humans@astronomer.io', subject='Hello', html_content='World!', conn_id='SMTP')\n

Parameters:

Name Type Description task_id str

The task_id for the operator

to str | list[str]

The recipient of the email

subject str

The subject of the email

html_content str

The content of the email

files list, optional

The files to attach to the email, defaults to None

conn_id str, optional

The SMTP connection to use. Defaults to \"SMTP\" and sets orbiter_conns property

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.sql","title":"orbiter.objects.operators.sql","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.sql.OrbiterSQLExecuteQueryOperator","title":"orbiter.objects.operators.sql.OrbiterSQLExecuteQueryOperator","text":"

Bases: OrbiterOperator

An Airflow Generic SQL Operator. Used to run SQL against any Database.

>>> OrbiterSQLExecuteQueryOperator(\n...   task_id=\"foo\", conn_id='sql', sql=\"select 1;\"\n... )\nfoo_task = SQLExecuteQueryOperator(task_id='foo', conn_id='sql', sql='select 1;')\n

Parameters:

Name Type Description task_id str

The task_id for the operator

conn_id str

The SQL connection to utilize. (Note: use the **conn_id(...) utility function)

sql str

The SQL to execute

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.ssh","title":"orbiter.objects.operators.ssh","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.ssh.OrbiterSSHOperator","title":"orbiter.objects.operators.ssh.OrbiterSSHOperator","text":"

Bases: OrbiterOperator

An Airflow SSHOperator. Used to run shell commands over SSH.

>>> OrbiterSSHOperator(task_id=\"foo\", ssh_conn_id=\"SSH\", command=\"echo 'hello world'\")\nfoo_task = SSHOperator(task_id='foo', ssh_conn_id='SSH', command=\"echo 'hello world'\")\n

Parameters:

Name Type Description task_id str

The task_id for the operator

ssh_conn_id str

The SSH connection to use. (Note: use the **conn_id(...) utility function)

command str

The command to execute

environment dict, optional

The environment variables to set, defaults to None

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

Astronomer Orbiter can land legacy workloads safely down in a new home on Apache Airflow!

"},{"location":"#what-is-orbiter","title":"What is Orbiter?","text":"

Orbiter is both a CLI and Framework for converting workflows from other orchestration tools to Apache Airflow.

Generally it can be thoughts of as:

flowchart LR\n    origin{{ XML/JSON/YAML/Etc Workflows }}\n    origin -->| \u2728 Translations \u2728 | airflow{{ Apache Airflow Project }}
The framework is a set of Rules and Objects that can translate workflows from an Origin system to an Airflow project.

"},{"location":"#installation","title":"Installation","text":"

You can install the orbiter CLI, if you have Python >= 3.10 installed via pip:

pip install astronomer-orbiter\n
If you do not have a compatible Python environment, pre-built binary executables of the orbiter CLI are available for download on the Releases page.

"},{"location":"#translate","title":"Translate","text":"

You can utilize the orbiter CLI with existing translations to convert workflows from other systems to Apache Airflow.

  1. Set up a new folder, and create a workflow/ folder. Add your workflows files to it
    .\n\u2514\u2500\u2500 workflow/\n    \u251c\u2500\u2500 workflow_a.json\n    \u251c\u2500\u2500 workflow_b.json\n    \u2514\u2500\u2500 ...\n
  2. Determine the specific translation ruleset via:
    1. the Origins documentation
    2. the orbiter help command
    3. or by creating a translation ruleset, if one does not exist
  3. Install the specific translation ruleset via the orbiter install command
  4. Use the orbiter translate command with the <RULESET> determined in the last step This will produce output to an output/ folder:
    orbiter translate workflow/ output/ --ruleset <RULESET>\n
  5. Review the contents of the output/ folder. If extensions or customizations are required, review how to extend a translation ruleset
  6. Utilize the astro CLI to run Airflow instance with your migrated workloads
  7. Deploy to Astro to run your translated workflows in production! \ud83d\ude80

You can see more specifics on how to use the Orbiter CLI in the CLI section.

"},{"location":"#authoring-rulesets-customization","title":"Authoring Rulesets & Customization","text":"

Orbiter can be extended to fit specific needs, patterns, or to support additional origins.

Read more specifics about how to use the framework at Rules and Objects

"},{"location":"#extend-or-customize","title":"Extend or Customize","text":"

To extend or customize an existing ruleset, you can easily modify it with simple Python code.

  1. Set up your workspace as described in steps 1+2 of the Translate instructions
  2. Create a Python script, named override.py
    .\n\u251c\u2500\u2500 override.py\n\u2514\u2500\u2500 workflow/\n    \u251c\u2500\u2500 workflow_a.json\n    \u251c\u2500\u2500 workflow_b.json\n    \u2514\u2500\u2500 ...\n
  3. Add contents to override.py: override.py

    from orbiter_community_translations.dag_factory import translation_ruleset  # (1)!\nfrom orbiter.objects.operators.ssh import OrbiterSSHOperator  # (2)!\nfrom orbiter.rules import task_rule  # (3)!\n\n\n@task_rule(priority=99)  # (4)!\ndef ssh_rule(val: dict):\n    \"\"\"Demonstration of overriding rulesets, by switching DAG Factory BashOperators to SSHOperators\"\"\"\n    if val.pop(\"operator\", \"\") == \"BashOperator\":  # (5)!\n        return OrbiterSSHOperator(  # (6)!\n            command=val.pop(\"bash_command\"),\n            doc=\"Hello World!\",\n            **{k: v for k, v in val if k != \"dependencies\"},\n        )\n    else:\n        return None\n\n\ntranslation_ruleset.task_ruleset.ruleset.append(ssh_rule)  # (7)!\n

    1. Importing specific translation ruleset, determined via the Origins page
    2. Importing required Objects
    3. Importing required Rule types
    4. Create one or more @rule functions, as required. A higher priority means this rule will be applied first. @task_rule Reference
    5. Rules have an if/else statement - they must always return a single thing or nothing
    6. OrbiterSSHOperator Reference
    7. Append the new Rule to the translation_ruleset
  4. Invoke the orbiter CLI, pointing it at your customized ruleset, and writing output to an output/ folder:

    orbiter translate workflow/ output/ --ruleset override.translation_ruleset\n

  5. Follow the remaining steps 4 -> 6 of the Translate instructions
"},{"location":"#authoring-a-new-ruleset","title":"Authoring a new Ruleset","text":"

You can utilize the Template TranslationRuleset as a starter, to create a new TranslationRuleset.

"},{"location":"#faq","title":"FAQ","text":"
  • Can this tool convert my workflows from tool X to Airflow?

    If you don't see your tool listed in Supported Origins, contact us for services to create translations, create an issue in the orbiter-community-translations repository, or write a TranslationRuleset and submit a pull request to share your translations with the community.

  • Are the results of this tool under any guarantee of correctness?

    No. This tool is provided as-is, with no guarantee of correctness. It is your responsibility to verify the results. We accept Pull Requests to improve parsing, and strive to make the tool easily configurable to handle your specific use-case.

Artwork Orbiter logo by Ivan Colic used with permission from The Noun Project under Creative Commons.

"},{"location":"cli/","title":"CLI","text":""},{"location":"cli/#orbiter","title":"orbiter","text":"

Orbiter is a CLI that converts other workflows to Airflow Projects.

Usage:

orbiter [OPTIONS] COMMAND [ARGS]...\n

Options:

Name Type Description Default --version boolean Show the version and exit. False --help boolean Show this message and exit. False"},{"location":"cli/#help","title":"help","text":"

List available Translation Rulesets

Usage:

orbiter help [OPTIONS]\n

Options:

Name Type Description Default --help boolean Show this message and exit. False"},{"location":"cli/#install","title":"install","text":"

Install a new Orbiter Translation Ruleset from a repository

Usage:

orbiter install [OPTIONS]\n

Options:

Name Type Description Default -r, --repo choice (astronomer-orbiter-translations | orbiter-community-translations) Choose a repository to install (will prompt, if not given) None -k, --key text [Optional] License Key to use for the translation ruleset.

Should look like 'AAAA-BBBB-1111-2222-3333-XXXX-YYYY-ZZZZ' | None | | --help | boolean | Show this message and exit. | False |

"},{"location":"cli/#translate","title":"translate","text":"

Translate workflows in an INPUT_DIR to an OUTPUT_DIR Airflow Project.

Provide a specific ruleset with the --ruleset flag.

Run orbiter help to see available rulesets.

INPUT_DIR defaults to $CWD/workflow.

OUTPUT_DIR defaults to $CWD/output

Usage:

orbiter translate [OPTIONS] INPUT_DIR OUTPUT_DIR\n

Options:

Name Type Description Default -r, --ruleset text Qualified name of a TranslationRuleset _required --format / --no-format boolean [optional] format the output with Ruff True --help boolean Show this message and exit. False"},{"location":"cli/#logging","title":"Logging","text":"

You can alter the verbosity of the CLI by setting the LOG_LEVEL environment variable. The default is INFO.

export LOG_LEVEL=DEBUG\n
"},{"location":"origins/","title":"Origins","text":"

An Origin is a source system that contains workflows that can be translated to an Apache Airflow project.

"},{"location":"origins/#supported-origins","title":"Supported Origins","text":"Origin Maintainer Repository Ruleset(s) Task Equivalent DAG Equivalent DAG Factory Community orbiter-community-translations orbiter_translations.dag_factory.yaml_base.translation_ruleset --- --- Control M Astronomer astronomer-orbiter-translations orbiter_translations.control_m.json_base.translation_ruleset Job Folder \u2800 \u2800 \u2800 orbiter_translations.control_m.json_ssh.translation_ruleset \u2800 \u2800 \u2800 \u2800 \u2800 orbiter_translations.control_m.xml_base.translation_ruleset \u2800 \u2800 \u2800 \u2800 \u2800 orbiter_translations.control_m.xml_ssh.translation_ruleset \u2800 \u2800 Automic Astronomer astronomer-orbiter-translations WIP Job Job Plan Autosys Astronomer astronomer-orbiter-translations WIP \u2800 \u2800 JAMS Astronomer astronomer-orbiter-translations WIP Job Folder\u2800 SSIS Astronomer astronomer-orbiter-translations orbiter_translations.ssis.xml_base.translation_ruleset \u2800 \u2800 Oozie Astronomer astronomer-orbiter-translations orbiter_translations.oozie.xml_base.translation_ruleset Node Workflow & more! \u2800 \u2800 \u2800 \u2800 \u2800

For Astronomer maintained Translation Rulesets, please contact us for access to the most up-to-date versions.

If you don't see your Origin system listed, please either:

  • contact us for services to create translations
  • create an issue in our orbiter-community-translations repository
  • write a TranslationRuleset and submit a pull request to share your translations with the community
"},{"location":"Rules_and_Rulesets/","title":"Overview","text":"

The brain of the Orbiter framework is in it's Rules and the Rulesets that contain them.

  • A Rule contains a python function that is evaluated and produces something (typically an Object) or nothing
  • A Ruleset is a collection of Rules that are evaluated in priority order
  • A TranslationRuleset is a collection of Rulesets, relating to an Origin and FileType, with a translate_fn which determines how to apply the rulesets.

Different Rules are applied in different scenarios; such as for converting input to a DAG (@dag_rule), or a specific Airflow Operator (@task_rule), or for filtering entries from the input data (@dag_filter_rule, @task_filter_rule).

Tip

To map the following input

{\n    \"id\": \"my_task\",\n    \"command\": \"echo 'hi'\"\n}\n

to an Airflow BashOperator, a Rule could parse it as follows:

@task_rule\ndef my_rule(val):\n    if 'command' in val:\n        return OrbiterBashOperator(task_id=val['id'], bash_command=val['command'])\n

This returns a OrbiterBashOperator, which will become an Airflow BashOperator when the translation completes.

"},{"location":"Rules_and_Rulesets/#orbiter.rules.rulesets.translate","title":"orbiter.rules.rulesets.translate","text":"
translate(\n    translation_ruleset, input_dir: Path\n) -> OrbiterProject\n

Orbiter expects a folder containing text files which may have a structure like:

{\"<workflow name>\": { ...<workflow properties>, \"<task name>\": { ...<task properties>} }}\n

The default translation function (orbiter.rules.rulesets.translate) performs the following steps:

  1. Find all files with the expected TranslationRuleset.file_type (.json, .xml, .yaml, etc.) in the input folder. Load each file and turn it into a Python Dictionary.
  2. For each file: Apply the TranslationRuleset.dag_filter_ruleset to filter down to entries that can translate to a DAG, in priority order.
    • For each: Apply the TranslationRuleset.dag_ruleset, to convert the object to an OrbiterDAG, in priority-order, stopping when the first rule returns a match. If no rule returns a match, the entry is filtered.
  3. Apply the TranslationRuleset.task_filter_ruleset to filter down to entries in the DAG that can translate to a Task, in priority-order.
    • For each: Apply the TranslationRuleset.task_ruleset, to convert the object to a specific Task, in priority-order, stopping when the first rule returns a match. If no rule returns a match, the entry is filtered.
  4. After the DAG and Tasks are mapped, the TranslationRuleset.task_dependency_ruleset is applied in priority-order, stopping when the first rule returns a match, to create a list of OrbiterTaskDependency, which are then added to each task in the OrbiterDAG
  5. Apply the TranslationRuleset.post_processing_ruleset, against the OrbiterProject, which can make modifications after all other rules have been applied.
  6. After translation - the OrbiterProject is rendered to the output folder.
Source code in orbiter/rules/rulesets.py
@validate_call\ndef translate(translation_ruleset, input_dir: Path) -> OrbiterProject:\n    \"\"\"\n    Orbiter expects a folder containing text files which may have a structure like:\n    ```json\n    {\"<workflow name>\": { ...<workflow properties>, \"<task name>\": { ...<task properties>} }}\n    ```\n\n    The default translation function (`orbiter.rules.rulesets.translate`) performs the following steps:\n\n    ![Diagram of Orbiter Translation](../orbiter_diagram.png)\n\n    1. **Find all files** with the expected\n        [`TranslationRuleset.file_type`][orbiter.rules.rulesets.TranslationRuleset]\n        (`.json`, `.xml`, `.yaml`, etc.) in the input folder. Load each file and turn it into a Python Dictionary.\n    2. **For each file:** Apply the [`TranslationRuleset.dag_filter_ruleset`][orbiter.rules.rulesets.DAGFilterRuleset]\n        to filter down to entries that can translate to a DAG, in priority order.\n        - **For each**: Apply the [`TranslationRuleset.dag_ruleset`][orbiter.rules.rulesets.DAGRuleset],\n        to convert the object to an [`OrbiterDAG`][orbiter.objects.dag.OrbiterDAG],\n        in priority-order, stopping when the first rule returns a match.\n        If no rule returns a match, the entry is filtered.\n    3. Apply the [`TranslationRuleset.task_filter_ruleset`][orbiter.rules.rulesets.TaskFilterRuleset]\n        to filter down to entries in the DAG that can translate to a Task, in priority-order.\n        - **For each:** Apply the [`TranslationRuleset.task_ruleset`][orbiter.rules.rulesets.TaskRuleset],\n            to convert the object to a specific Task, in priority-order, stopping when the first rule returns a match.\n            If no rule returns a match, the entry is filtered.\n    4. After the DAG and Tasks are mapped, the\n        [`TranslationRuleset.task_dependency_ruleset`][orbiter.rules.rulesets.TaskDependencyRuleset]\n        is applied in priority-order, stopping when the first rule returns a match,\n        to create a list of\n        [`OrbiterTaskDependency`][orbiter.objects.task.OrbiterTaskDependency],\n        which are then added to each task in the\n        [`OrbiterDAG`][orbiter.objects.dag.OrbiterDAG]\n    5. Apply the [`TranslationRuleset.post_processing_ruleset`][orbiter.rules.rulesets.PostProcessingRuleset],\n        against the [`OrbiterProject`][orbiter.objects.project.OrbiterProject], which can make modifications after all\n        other rules have been applied.\n    6. After translation - the [`OrbiterProject`][orbiter.objects.project.OrbiterProject]\n        is rendered to the output folder.\n\n\n    \"\"\"\n\n    def _get_files_with_extension(_extension: str, _input_dir: Path) -> List[Path]:\n        return [\n            directory / file\n            for (directory, _, files) in _input_dir.walk()\n            for file in files\n            if _extension == file.lower()[-len(_extension) :]\n        ]\n\n    if not isinstance(translation_ruleset, TranslationRuleset):\n        raise RuntimeError(\n            f\"Error! type(translation_ruleset)=={type(translation_ruleset)}!=TranslationRuleset! Exiting!\"\n        )\n\n    # Create an initial OrbiterProject\n    project = OrbiterProject()\n\n    extension = translation_ruleset.file_type.value.lower()\n\n    logger.info(f\"Finding files with extension={extension} in {input_dir}\")\n    files = _get_files_with_extension(extension, input_dir)\n\n    # .yaml is sometimes '.yml'\n    if extension == \"yaml\":\n        files.extend(_get_files_with_extension(\"yml\", input_dir))\n\n    logger.info(f\"Found {len(files)} files with extension={extension} in {input_dir}\")\n\n    for file in files:\n        logger.info(f\"Translating file={file.resolve()}\")\n\n        # Load the file and convert it into a python dict\n        input_dict = load_filetype(file.read_text(), translation_ruleset.file_type)\n\n        # DAG FILTER Ruleset - filter down to keys suspected of being translatable to a DAG, in priority order.\n        dag_dicts = functools.reduce(\n            add,\n            translation_ruleset.dag_filter_ruleset.apply(val=input_dict),\n            [],\n        )\n        logger.debug(f\"Found {len(dag_dicts)} DAG candidates in {file.resolve()}\")\n        for dag_dict in dag_dicts:\n            # DAG Ruleset - convert the object to an `OrbiterDAG` via `dag_ruleset`,\n            #         in priority-order, stopping when the first rule returns a match\n            dag: OrbiterDAG | None = translation_ruleset.dag_ruleset.apply(\n                val=dag_dict,\n                take_first=True,\n            )\n            if dag is None:\n                logger.warning(\n                    f\"Couldn't extract DAG from dag_dict={dag_dict} with dag_ruleset={translation_ruleset.dag_ruleset}\"\n                )\n                continue\n            dag.orbiter_kwargs[\"file_path\"] = str(file.resolve())\n\n            tasks = {}\n            # TASK FILTER Ruleset - Many entries in dag_dict -> Many task_dict\n            task_dicts = functools.reduce(\n                add,\n                translation_ruleset.task_filter_ruleset.apply(val=dag_dict),\n                [],\n            )\n            logger.debug(\n                f\"Found {len(task_dicts)} Task candidates in {dag.dag_id} in {file.resolve()}\"\n            )\n            for task_dict in task_dicts:\n                # TASK Ruleset one -> one\n                task: OrbiterOperator = translation_ruleset.task_ruleset.apply(\n                    val=task_dict, take_first=True\n                )\n                if task is None:\n                    logger.warning(\n                        f\"Couldn't extract task from expected task_dict={task_dict}\"\n                    )\n                    continue\n\n                _add_task_deduped(task, tasks)\n            logger.debug(f\"Adding {len(tasks)} tasks to DAG {dag.dag_id}\")\n            dag.add_tasks(tasks.values())\n\n            # Dag-Level TASK DEPENDENCY Ruleset\n            task_dependencies: List[OrbiterTaskDependency] = (\n                list(chain(*translation_ruleset.task_dependency_ruleset.apply(val=dag)))\n                or []\n            )\n            if not len(task_dependencies):\n                logger.warning(f\"Couldn't find task dependencies in dag={dag_dict}\")\n            for task_dependency in task_dependencies:\n                task_dependency: OrbiterTaskDependency\n                if task_dependency.task_id not in dag.tasks:\n                    logger.warning(\n                        f\"Couldn't find task_id={task_dependency.task_id} in tasks={tasks} for dag_id={dag.dag_id}\"\n                    )\n                    continue\n                else:\n                    dag.tasks[task_dependency.task_id].add_downstream(task_dependency)\n\n            logger.debug(f\"Adding DAG {dag.dag_id} to project\")\n            project.add_dags(dag)\n\n    # POST PROCESSING Ruleset\n    translation_ruleset.post_processing_ruleset.apply(val=project, take_first=False)\n\n    return project\n
"},{"location":"Rules_and_Rulesets/#orbiter.rules.rulesets.load_filetype","title":"orbiter.rules.rulesets.load_filetype","text":"
load_filetype(input_str: str, file_type: FileType) -> dict\n

Orbiter converts all file types into a Python dictionary \"intermediate representation\" form, prior to any rulesets being applied.

FileType Conversion Method XML xmltodict_parse YAML yaml.safe_load JSON json.loads Source code in orbiter/rules/rulesets.py
@validate_call\ndef load_filetype(input_str: str, file_type: FileType) -> dict:\n    \"\"\"\n    Orbiter converts all file types into a Python dictionary \"intermediate representation\" form,\n    prior to any rulesets being applied.\n\n    | FileType | Conversion Method                                           |\n    |----------|-------------------------------------------------------------|\n    | `XML`    | [`xmltodict_parse`][orbiter.rules.rulesets.xmltodict_parse] |\n    | `YAML`   | `yaml.safe_load`                                            |\n    | `JSON`   | `json.loads`                                                |\n    \"\"\"\n\n    if file_type == FileType.JSON:\n        import json\n\n        return json.loads(input_str)\n    elif file_type == FileType.YAML:\n        import yaml\n\n        return yaml.safe_load(input_str)\n    elif file_type == FileType.XML:\n        return xmltodict_parse(input_str)\n    else:\n        raise NotImplementedError(f\"Cannot load file_type={file_type}\")\n
"},{"location":"Rules_and_Rulesets/#orbiter.rules.rulesets.xmltodict_parse","title":"orbiter.rules.rulesets.xmltodict_parse","text":"
xmltodict_parse(input_str: str) -> Any\n

Calls xmltodict.parse and does post-processing fixes.

Note

The original xmltodict.parse method returns EITHER:

  • a dict (one child element of type)
  • or a list of dict (many child element of type)

This behavior can be confusing, and is an issue with the original xml spec being referenced.

This method deviates by standardizing to the latter case (always a list[dict]).

All XML elements will be a list of dictionaries, even if there's only one element.

>>> xmltodict_parse(\"\")\nTraceback (most recent call last):\nxml.parsers.expat.ExpatError: no element found: line 1, column 0\n>>> xmltodict_parse(\"<a></a>\")\n{'a': None}\n>>> xmltodict_parse(\"<a foo='bar'></a>\")\n{'a': [{'@foo': 'bar'}]}\n>>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'></foo></a>\")  # Singleton - gets modified\n{'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}]}]}\n>>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'><bar><bop></bop></bar></foo></a>\")  # Nested Singletons - modified\n{'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz', 'bar': [{'bop': None}]}]}]}\n>>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'></foo><foo bing='bop'></foo></a>\")\n{'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}, {'@bing': 'bop'}]}]}\n

Parameters:

Name Type Description input_str str

The XML string to parse

Returns:

Type Description dict

The parsed XML

Source code in orbiter/rules/rulesets.py
def xmltodict_parse(input_str: str) -> Any:\n    \"\"\"Calls `xmltodict.parse` and does post-processing fixes.\n\n    !!! note\n\n        The original [`xmltodict.parse`](https://pypi.org/project/xmltodict/) method returns EITHER:\n\n        - a dict (one child element of type)\n        - or a list of dict (many child element of type)\n\n        This behavior can be confusing, and is an issue with the original xml spec being referenced.\n\n        **This method deviates by standardizing to the latter case (always a `list[dict]`).**\n\n        **All XML elements will be a list of dictionaries, even if there's only one element.**\n\n    ```pycon\n    >>> xmltodict_parse(\"\")\n    Traceback (most recent call last):\n    xml.parsers.expat.ExpatError: no element found: line 1, column 0\n    >>> xmltodict_parse(\"<a></a>\")\n    {'a': None}\n    >>> xmltodict_parse(\"<a foo='bar'></a>\")\n    {'a': [{'@foo': 'bar'}]}\n    >>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'></foo></a>\")  # Singleton - gets modified\n    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}]}]}\n    >>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'><bar><bop></bop></bar></foo></a>\")  # Nested Singletons - modified\n    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz', 'bar': [{'bop': None}]}]}]}\n    >>> xmltodict_parse(\"<a foo='bar'><foo bar='baz'></foo><foo bing='bop'></foo></a>\")\n    {'a': [{'@foo': 'bar', 'foo': [{'@bar': 'baz'}, {'@bing': 'bop'}]}]}\n\n    ```\n    :param input_str: The XML string to parse\n    :type input_str: str\n    :return: The parsed XML\n    :rtype: dict\n    \"\"\"\n    import xmltodict\n\n    # noinspection t\n    def _fix(d):\n        \"\"\"fix the dict in place, recursively, standardizing on a list of dict even if there's only one entry.\"\"\"\n        # if it's a dict, descend to fix\n        if isinstance(d, dict):\n            for k, v in d.items():\n                # @keys are properties of elements, non-@keys are elements\n                if not k.startswith(\"@\"):\n                    if isinstance(v, dict):\n                        # THE FIX\n                        # any non-@keys should be a list of dict, even if there's just one of the element\n                        d[k] = [v]\n                        _fix(v)\n                    else:\n                        _fix(v)\n        # if it's a list, descend to fix\n        if isinstance(d, list):\n            for v in d:\n                _fix(v)\n\n    output = xmltodict.parse(input_str)\n    _fix(output)\n    return output\n
"},{"location":"Rules_and_Rulesets/rules/","title":"Rules","text":""},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.Rule","title":"orbiter.rules.Rule","text":"

A Rule contains a python function that is evaluated and produces something (typically an Object) or nothing

A Rule can be created from a decorator

>>> @rule(priority=1)\n... def my_rule(val):\n...     return 1\n>>> isinstance(my_rule, Rule)\nTrue\n>>> my_rule(val={})\n1\n

The function in a rule takes one parameter (val), and must always evaluate to something or nothing.

>>> Rule(rule=lambda val: 4)({})\n4\n>>> Rule(rule=lambda val: None)({})\n

Tip

If the returned value is an Orbiter Object, the passed kwargs are saved in a special orbiter_kwargs property

>>> from orbiter.objects.dag import OrbiterDAG\n>>> @rule\n... def my_rule(foo):\n...     return OrbiterDAG(dag_id=\"\", file_path=\"\")\n>>> my_rule(foo=\"bar\").orbiter_kwargs\n{'foo': 'bar'}\n

Note

A Rule must have a rule property and extra properties cannot be passed

>>> # noinspection Pydantic\n... Rule(rule=lambda: None, not_a_prop=\"???\")\n... # doctest: +ELLIPSIS\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description rule Callable[[dict | Any], Any | None]

Python function to evaluate. Takes a single argument and returns something or nothing

priority int, optional

Higher priority rules are evaluated first, must be greater than 0. Default is 0

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.DAGFilterRule","title":"orbiter.rules.DAGFilterRule","text":"

Bases: Rule

The @dag_filter_rule decorator creates a DAGFilterRule

@dag_filter_rule\ndef foo(val: dict) -> List[dict]:\n    return [{\"dag_id\": \"foo\"}]\n

Hint

In addition to filtering, a DAGFilterRule can also map input to a more reasonable output for later processing

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.DAGRule","title":"orbiter.rules.DAGRule","text":"

Bases: Rule

A @dag_rule decorator creates a DAGRule

@dag_rule\ndef foo(val: dict) -> OrbiterDAG | None:\n    if 'id' in val:\n        return OrbiterDAG(dag_id=val[\"id\"], file_path=f\"{val[\"id\"]}.py\")\n    else:\n        return None\n
"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.TaskFilterRule","title":"orbiter.rules.TaskFilterRule","text":"

Bases: Rule

A @task_filter_rule decorator creates a TaskFilterRule

@task_filter_rule\ndef foo(val: dict) -> List[dict] | None:\n    return [{\"task_id\": \"foo\"}]\n

Hint

In addition to filtering, a TaskFilterRule can also map input to a more reasonable output for later processing

Parameters:

Name Type Description val dict

A dictionary of the task

Returns:

Type Description List[dict] | None

A list of dictionaries of possible tasks or None

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.TaskRule","title":"orbiter.rules.TaskRule","text":"

Bases: Rule

A @task_rule decorator creates a TaskRule

@task_rule\ndef foo(val: dict) -> OrbiterOperator | OrbiterTaskGroup:\n    if 'id' in val and 'command' in val:\n        return OrbiterBashOperator(task_id=val['id'], bash_command=val['command'])\n    else:\n        return None\n

Parameters:

Name Type Description val dict

A dictionary of the task

Returns:

Type Description OrbiterOperator | OrbiterTaskGroup | None

A subclass of OrbiterOperator or OrbiterTaskGroup or None

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.TaskDependencyRule","title":"orbiter.rules.TaskDependencyRule","text":"

Bases: Rule

An @task_dependency_rule decorator creates a TaskDependencyRule, which takes an OrbiterDAG and returns a list[OrbiterTaskDependency] or None

@task_dependency_rule\ndef foo(val: OrbiterDAG) -> OrbiterTaskDependency:\n    return [OrbiterTaskDependency(task_id=\"upstream\", downstream=\"downstream\")]\n

Parameters:

Name Type Description val OrbiterDAG

An OrbiterDAG

Returns:

Type Description List[OrbiterTaskDependency] | None

A list of OrbiterTaskDependency or None

"},{"location":"Rules_and_Rulesets/rules/#orbiter.rules.PostProcessingRule","title":"orbiter.rules.PostProcessingRule","text":"

Bases: Rule

An @post_processing_rule decorator creates a PostProcessingRule, which takes an OrbiterProject, after all other rules have been applied, and modifies it in-place.

@post_processing_rule\ndef foo(val: OrbiterProject) -> None:\n    val.dags[\"foo\"].tasks[\"bar\"].doc = \"Hello World\"\n

Parameters:

Name Type Description val OrbiterProject

An OrbiterProject

Returns:

Type Description None

None

"},{"location":"Rules_and_Rulesets/rulesets/","title":"Rulesets","text":""},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.TranslationRuleset","title":"orbiter.rules.rulesets.TranslationRuleset","text":"

A Ruleset is a collection of Rules that are evaluated in priority order

A TranslationRuleset is a container for Rulesets, which applies to a specific translation

>>> TranslationRuleset(\n...   file_type=FileType.JSON,                                      # Has a file type\n...   translate_fn=fake_translate,                                  # and can have a callable\n...   # translate_fn=\"orbiter.rules.translate.fake_translate\",      # or a qualified name to a function\n...   dag_filter_ruleset={\"ruleset\": [{\"rule\": lambda x: None}]},   # Rulesets can be dict within dicts\n...   dag_ruleset=DAGRuleset(ruleset=[Rule(rule=lambda x: None)]),  # or objects within objects\n...   task_filter_ruleset=EMPTY_RULESET,                            # or a mix\n...   task_ruleset=EMPTY_RULESET,\n...   task_dependency_ruleset=EMPTY_RULESET,                        # Omitted for brevity\n...   post_processing_ruleset=EMPTY_RULESET,\n... )\nTranslationRuleset(...)\n

Parameters:

Name Type Description file_type FileType

FileType to translate (.json, .xml, .yaml, etc.)

dag_filter_ruleset DAGFilterRuleset | dict

DAGFilterRuleset (of DAGFilterRule)

dag_ruleset DAGRuleset | dict

DAGRuleset (of DAGRules)

task_filter_ruleset TaskFilterRuleset | dict

TaskFilterRule (of TaskFilterRule)

task_ruleset TaskRuleset | dict

TaskRuleset (of TaskRules)

task_dependency_ruleset TaskDependencyRuleset | dict

TaskDependencyRuleset (of TaskDependencyRules)

post_processing_ruleset PostProcessingRuleset | dict

PostProcessingRuleset (of PostProcessingRules)

translate_fn Callable[[TranslationRuleset, Path], OrbiterProject] | str | TranslateFn

Either a qualified name to a function (e.g. path.to.file.function), or a function reference, with the signature: (translation_ruleset: Translation Ruleset, input_dir: Path) -> OrbiterProject

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.Ruleset","title":"orbiter.rules.rulesets.Ruleset","text":"

A list of rules, which are evaluated to generate different types of output

You must pass a Rule (or dict with the schema of Rule)

>>> from orbiter.rules import rule\n>>> @rule\n... def x(val):\n...    return None\n>>> Ruleset(ruleset=[x, {\"rule\": lambda: None}])\n... # doctest: +ELLIPSIS\nRuleset(ruleset=[Rule(...), Rule(...)])\n

Note

You can't pass non-Rules

>>> # noinspection PyTypeChecker\n... Ruleset(ruleset=[None])\n... # doctest: +ELLIPSIS\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description ruleset List[Rule | Callable[[Any], Any | None]]

List of Rule (or dict with the schema of Rule)

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.Ruleset.apply","title":"apply","text":"
apply(\n    take_first: bool = False, **kwargs\n) -> List[Any] | Any\n

Apply all rules in ruleset to a single item, in priority order, removing any None results.

A ruleset with one rule can produce up to one result

>>> from orbiter.rules import rule\n\n>>> @rule\n... def gt_4(val):\n...     return str(val) if val > 4 else None\n>>> Ruleset(ruleset=[gt_4]).apply(val=5)\n['5']\n

Many rules can produce many results, one for each rule.

>>> @rule\n... def gt_3(val):\n...    return str(val) if val > 3 else None\n>>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5)\n['5', '5']\n

The take_first flag will evaluate rules in the ruleset and return the first match

>>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5, take_first=True)\n'5'\n

If nothing matched, an empty list is returned

>>> @rule\n... def always_none(val):\n...     return None\n>>> @rule\n... def more_always_none(val):\n...     return None\n>>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5)\n[]\n

If nothing matched, and take_first=True, None is returned

>>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5, take_first=True)\n... # None\n

Tip

If no input is given, an error is returned

>>> Ruleset(ruleset=[always_none]).apply()\nTraceback (most recent call last):\nRuntimeError: No values provided! Supply at least one key=val pair as kwargs!\n

Parameters:

Name Type Description take_first bool

only take the first (if any) result from the ruleset application

kwargs

key=val pairs to pass to the evaluated rule function

Returns:

Type Description List[Any] | Any | None

List of rules that evaluated to Any (in priority order), or an empty list, or Any (if take_first=True)

Raises:

Type Description RuntimeError

if the Ruleset is empty or input_val is None

RuntimeError

if the Rule raises an exception

Source code in orbiter/rules/rulesets.py
@validate_call\ndef apply(self, take_first: bool = False, **kwargs) -> List[Any] | Any:\n    \"\"\"\n    Apply all rules in ruleset **to a single item**, in priority order, removing any `None` results.\n\n    A ruleset with one rule can produce **up to one** result\n    ```pycon\n    >>> from orbiter.rules import rule\n\n    >>> @rule\n    ... def gt_4(val):\n    ...     return str(val) if val > 4 else None\n    >>> Ruleset(ruleset=[gt_4]).apply(val=5)\n    ['5']\n\n    ```\n\n    Many rules can produce many results, one for each rule.\n    ```pycon\n    >>> @rule\n    ... def gt_3(val):\n    ...    return str(val) if val > 3 else None\n    >>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5)\n    ['5', '5']\n\n    ```\n\n    The `take_first` flag will evaluate rules in the ruleset and return the first match\n    ```pycon\n    >>> Ruleset(ruleset=[gt_4, gt_3]).apply(val=5, take_first=True)\n    '5'\n\n    ```\n\n    If nothing matched, an empty list is returned\n    ```pycon\n    >>> @rule\n    ... def always_none(val):\n    ...     return None\n    >>> @rule\n    ... def more_always_none(val):\n    ...     return None\n    >>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5)\n    []\n\n    ```\n\n    If nothing matched, and `take_first=True`, `None` is returned\n    ```pycon\n    >>> Ruleset(ruleset=[always_none, more_always_none]).apply(val=5, take_first=True)\n    ... # None\n\n    ```\n\n    !!! tip\n\n        If no input is given, an error is returned\n        ```pycon\n        >>> Ruleset(ruleset=[always_none]).apply()\n        Traceback (most recent call last):\n        RuntimeError: No values provided! Supply at least one key=val pair as kwargs!\n\n        ```\n\n    :param take_first: only take the first (if any) result from the ruleset application\n    :type take_first: bool\n    :param kwargs: key=val pairs to pass to the evaluated rule function\n    :returns: List of rules that evaluated to `Any` (in priority order),\n                or an empty list,\n                or `Any` (if `take_first=True`)\n    :rtype: List[Any] | Any | None\n    :raises RuntimeError: if the Ruleset is empty or input_val is None\n    :raises RuntimeError: if the Rule raises an exception\n    \"\"\"\n    if not len(kwargs):\n        raise RuntimeError(\n            \"No values provided! Supply at least one key=val pair as kwargs!\"\n        )\n    results = []\n    for _rule in self._sorted():\n        result = _rule(**kwargs)\n        should_show_input = \"val\" in kwargs and not (\n            isinstance(kwargs[\"val\"], OrbiterProject)\n            or isinstance(kwargs[\"val\"], OrbiterDAG)\n        )\n        if result is not None:\n            logger.debug(\n                \"---------\\n\"\n                f\"[RULESET MATCHED] '{self.__class__.__module__}.{self.__class__.__name__}'\\n\"\n                f\"[RULE MATCHED] '{_rule.__name__}'\\n\"\n                f\"[INPUT] {kwargs if should_show_input else '<Skipping...>'}\\n\"\n                f\"[RETURN] {result}\\n\"\n                f\"---------\"\n            )\n            results.append(result)\n            if take_first:\n                return result\n    return None if take_first and not len(results) else results\n
"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.Ruleset.apply_many","title":"apply_many","text":"
apply_many(\n    input_val: Collection[Any], take_first: bool = False\n) -> List[List[Any]] | List[Any]\n

Apply a ruleset to each item in collection (such as dict().items()) and return any results that are not None

You can turn the output of apply_many into a dict, if the rule takes and returns a tuple

>>> from itertools import chain\n>>> from orbiter.rules import rule\n\n>>> @rule\n... def filter_for_type_folder(val):\n...   (key, val) = val\n...   return (key, val) if val.get('Type', '') == 'Folder' else None\n>>> ruleset = Ruleset(ruleset=[filter_for_type_folder])\n>>> input_dict = {\n...    \"a\": {\"Type\": \"Folder\"},\n...    \"b\": {\"Type\": \"File\"},\n...    \"c\": {\"Type\": \"Folder\"},\n... }\n>>> dict(chain(*chain(ruleset.apply_many(input_dict.items()))))\n... # use dict(chain(*chain(...))), if using `take_first=True`, to turn many results back into dict\n{'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}\n>>> dict(ruleset.apply_many(input_dict.items(), take_first=True))\n... # use dict(...) directly, if using `take_first=True`, to turn results back into dict\n{'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}\n

You cannot pass input without length

>>> ruleset.apply_many({})\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\nRuntimeError: Input is not Collection[Any] with length!\n

Parameters:

Name Type Description input_val Collection[Any]

List to evaluate ruleset over

take_first bool

Only take the first (if any) result from each ruleset application

Returns:

Type Description List[List[Any]] | List[Any]

List of list with all non-null evaluations for each item or list of the first non-null evaluation for each item (if take_first=True)

Raises:

Type Description RuntimeError

if the Ruleset or input_vals are empty

RuntimeError

if the Rule raises an exception

Source code in orbiter/rules/rulesets.py
def apply_many(\n    self,\n    input_val: Collection[Any],\n    take_first: bool = False,\n) -> List[List[Any]] | List[Any]:\n    \"\"\"\n    Apply a ruleset to each item in collection (such as `dict().items()`)\n    and return any results that are not `None`\n\n    You can turn the output of `apply_many` into a dict, if the rule takes and returns a tuple\n    ```pycon\n    >>> from itertools import chain\n    >>> from orbiter.rules import rule\n\n    >>> @rule\n    ... def filter_for_type_folder(val):\n    ...   (key, val) = val\n    ...   return (key, val) if val.get('Type', '') == 'Folder' else None\n    >>> ruleset = Ruleset(ruleset=[filter_for_type_folder])\n    >>> input_dict = {\n    ...    \"a\": {\"Type\": \"Folder\"},\n    ...    \"b\": {\"Type\": \"File\"},\n    ...    \"c\": {\"Type\": \"Folder\"},\n    ... }\n    >>> dict(chain(*chain(ruleset.apply_many(input_dict.items()))))\n    ... # use dict(chain(*chain(...))), if using `take_first=True`, to turn many results back into dict\n    {'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}\n    >>> dict(ruleset.apply_many(input_dict.items(), take_first=True))\n    ... # use dict(...) directly, if using `take_first=True`, to turn results back into dict\n    {'a': {'Type': 'Folder'}, 'c': {'Type': 'Folder'}}\n\n    ```\n\n    You cannot pass input without length\n    ```pycon\n    >>> ruleset.apply_many({})\n    ... # doctest: +IGNORE_EXCEPTION_DETAIL\n    Traceback (most recent call last):\n    RuntimeError: Input is not Collection[Any] with length!\n\n    ```\n    :param input_val: List to evaluate ruleset over\n    :type input_val: Collection[Any]\n    :param take_first: Only take the first (if any) result from each ruleset application\n    :type take_first: bool\n    :returns: List of list with all non-null evaluations for each item<br>\n              or list of the first non-null evaluation for each item (if `take_first=True`)\n    :rtype: List[List[Any]] | List[Any]\n    :raises RuntimeError: if the Ruleset or input_vals are empty\n    :raises RuntimeError: if the Rule raises an exception\n    \"\"\"\n    # Validate Input\n    if not input_val or not len(input_val):\n        raise RuntimeError(\"Input is not `Collection[Any]` with length!\")\n\n    return [\n        results[0] if take_first else results\n        for item in input_val\n        if (results := self.apply(take_first=False, val=item)) is not None\n        and len(results)\n    ]\n
"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.DAGFilterRuleset","title":"orbiter.rules.rulesets.DAGFilterRuleset","text":"

Bases: Ruleset

Ruleset of DAGFilterRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.DAGRuleset","title":"orbiter.rules.rulesets.DAGRuleset","text":"

Bases: Ruleset

Ruleset of DAGRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.TaskFilterRuleset","title":"orbiter.rules.rulesets.TaskFilterRuleset","text":"

Bases: Ruleset

Ruleset of TaskFilterRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.TaskRuleset","title":"orbiter.rules.rulesets.TaskRuleset","text":"

Bases: Ruleset

Ruleset of TaskRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.TaskDependencyRuleset","title":"orbiter.rules.rulesets.TaskDependencyRuleset","text":"

Bases: Ruleset

Ruleset of TaskDependencyRule

"},{"location":"Rules_and_Rulesets/rulesets/#orbiter.rules.rulesets.PostProcessingRuleset","title":"orbiter.rules.rulesets.PostProcessingRuleset","text":"

Bases: Ruleset

Ruleset of PostProcessingRule

"},{"location":"Rules_and_Rulesets/template/","title":"Template","text":"

The following template can be utilized for creating a new TranslationRuleset

translation_template.py
from __future__ import annotations\nfrom orbiter import FileType\nfrom orbiter.objects.dag import OrbiterDAG\nfrom orbiter.objects.operators.empty import OrbiterEmptyOperator\nfrom orbiter.objects.project import OrbiterProject\nfrom orbiter.objects.task import OrbiterOperator\nfrom orbiter.objects.task_group import OrbiterTaskGroup\nfrom orbiter.rules import (\n    dag_filter_rule,\n    dag_rule,\n    task_filter_rule,\n    task_rule,\n    task_dependency_rule,\n    post_processing_rule,\n)\nfrom orbiter.rules.rulesets import (\n    DAGFilterRuleset,\n    DAGRuleset,\n    TaskFilterRuleset,\n    TaskRuleset,\n    TaskDependencyRuleset,\n    PostProcessingRuleset,\n    TranslationRuleset,\n)\n\n\n@dag_filter_rule\ndef basic_dag_filter(val: dict) -> list | None:\n    \"\"\"Filter input down to a list of dictionaries that can be processed by the `@dag_rules`\"\"\"\n    for k, v in val.items():\n        pass\n    return []\n\n\n@dag_rule\ndef basic_dag_rule(val: dict) -> OrbiterDAG | None:\n    \"\"\"Translate input into an `OrbiterDAG`\"\"\"\n    if \"dag_id\" in val:\n        return OrbiterDAG(dag_id=val[\"dag_id\"], file_path=\"file.py\")\n    else:\n        return None\n\n\n@task_filter_rule\ndef basic_task_filter(val: dict) -> list | None:\n    \"\"\"Filter input down to a list of dictionaries that can be processed by the `@task_rules`\"\"\"\n    for k, v in val.items():\n        pass\n    return []\n\n\n@task_rule(priority=2)\ndef basic_task_rule(val: dict) -> OrbiterOperator | OrbiterTaskGroup | None:\n    \"\"\"Translate input into an Operator (e.g. `OrbiterBashOperator`). will be applied first, with a higher priority\"\"\"\n    if \"task_id\" in val:\n        return OrbiterEmptyOperator(task_id=val[\"task_id\"])\n    else:\n        return None\n\n\n@task_rule(priority=1)\ndef cannot_map_rule(val: dict) -> OrbiterOperator | OrbiterTaskGroup | None:\n    \"\"\"This rule returns an `OrbiterEmptyOperator` with a doc string that says it cannot map the task,\n    so we can still see the task in the output. With a priority=1 it will be applied last\n    \"\"\"\n    import json\n\n    # noinspection PyArgumentList\n    return OrbiterEmptyOperator(\n        task_id=val[\"task_id\"], doc_md=f\"Cannot map task! input: {json.dumps(val)}\"\n    )\n\n\n@task_dependency_rule\ndef basic_task_dependency_rule(val: OrbiterDAG) -> list | None:\n    \"\"\"Translate input into a list of task dependencies\"\"\"\n    for task_dependency in val.orbiter_kwargs[\"task_dependencies\"]:\n        pass\n    return []\n\n\n@post_processing_rule\ndef basic_post_processing_rule(val: OrbiterProject) -> None:\n    \"\"\"Modify the project in-place, after all other rules have applied\"\"\"\n    for dag_id, dag in val.dags.items():\n        for task_id, task in dag.tasks.items():\n            pass\n\n\ntranslation_ruleset = TranslationRuleset(\n    file_type=FileType.JSON,\n    dag_filter_ruleset=DAGFilterRuleset(ruleset=[basic_dag_filter]),\n    dag_ruleset=DAGRuleset(ruleset=[basic_dag_rule]),\n    task_filter_ruleset=TaskFilterRuleset(ruleset=[basic_task_filter]),\n    task_ruleset=TaskRuleset(ruleset=[basic_task_rule, cannot_map_rule]),\n    task_dependency_ruleset=TaskDependencyRuleset(ruleset=[basic_task_dependency_rule]),\n    post_processing_ruleset=PostProcessingRuleset(ruleset=[basic_post_processing_rule]),\n)\n
"},{"location":"objects/","title":"Overview","text":"

Objects are returned from Rules during a translation, and are rendered to produce an Apache Airflow Project

An OrbiterProject holds everything necessary to render an Airflow Project. This is generated by a TranslationRuleset.translate_fn.

Workflows are represented by a OrbiterDAG which is a Directed Acyclic Graph (of Tasks).

OrbiterOperators represent Airflow Tasks, which are units of work. An Operator is a pre-defined task with specific functionality.

"},{"location":"objects/#orbiter.objects.OrbiterBase","title":"orbiter.objects.OrbiterBase","text":"

AbstractBaseClass for Orbiter objects, provides a number of properties

Parameters:

Name Type Description imports List[OrbiterRequirement]

List of OrbiterRequirement objects

orbiter_kwargs dict, optional

Optional dictionary of keyword arguments, to preserve what was originally parsed by a rule

orbiter_conns Set[OrbiterConnection], optional

Optional set of OrbiterConnection objects

orbiter_vars Set[OrbiterVariable], optional

Optional set of OrbiterVariable objects

orbiter_env_vars Set[OrbiterEnvVar], optional

Optional set of OrbiterEnvVar objects

orbiter_includes Set[OrbiterInclude], optional

Optional set of OrbiterInclude objects

"},{"location":"objects/dags/","title":"Workflow","text":"

Airflow workflows are represented by a DAG which is a Directed Acyclic Graph (of Tasks).

"},{"location":"objects/dags/#diagram","title":"Diagram","text":"
classDiagram\n    direction LR\n    class OrbiterRequirement[\"orbiter.objects.requirement.OrbiterRequirement\"] {\n            package: str | None\n            module: str | None\n            names: List[str] | None\n            sys_package: str | None\n    }\n\n    OrbiterDAG \"via schedule\" --> OrbiterTimetable\n    OrbiterDAG --> \"many\" OrbiterOperator\n    OrbiterDAG --> \"many\" OrbiterTaskGroup\n    OrbiterDAG --> \"many\" OrbiterRequirement\n    class OrbiterDAG[\"orbiter.objects.dag.OrbiterDAG\"] {\n            imports: List[OrbiterRequirement]\n            file_path: str\n            dag_id: str\n            schedule: str | OrbiterTimetable | None\n            catchup: bool\n            start_date: DateTime\n            tags: List[str]\n            default_args: Dict[str, Any]\n            params: Dict[str, Any]\n            doc_md: str | None\n            tasks: Dict[str, OrbiterOperator]\n            orbiter_kwargs: dict\n            orbiter_conns: Set[OrbiterConnection]\n            orbiter_vars: Set[OrbiterVariable]\n            orbiter_env_vars: Set[OrbiterEnvVar]\n            orbiter_includes: Set[OrbiterInclude]\n    }\n    click OrbiterDAG href \"#orbiter.objects.dag.OrbiterDAG\" \"OrbiterDAG Documentation\"\n\n    OrbiterTaskGroup --> \"many\" OrbiterRequirement\n    class OrbiterTaskGroup[\"orbiter.objects.task.OrbiterTaskGroup\"] {\n            task_group_id: str\n            tasks: List[OrbiterOperator | OrbiterTaskGroup]\n            add_downstream(str | List[str] | OrbiterTaskDependency)\n    }\n\n    OrbiterOperator --> \"many\" OrbiterRequirement\n    OrbiterOperator --> \"one\" OrbiterPool\n    OrbiterOperator --> \"many\" OrbiterConnection\n    OrbiterOperator --> \"many\" OrbiterVariable\n    OrbiterOperator --> \"many\" OrbiterEnvVar\n    OrbiterOperator --> \"many\" OrbiterTaskDependency\n    class OrbiterOperator[\"orbiter.objects.task.OrbiterOperator\"] {\n            imports: List[OrbiterRequirement]\n            operator: str\n            task_id: str\n            pool: str | None\n            pool_slots: int | None\n            trigger_rule: str | None\n            downstream: Set[OrbiterTaskDependency]\n            add_downstream(str | List[str] | OrbiterTaskDependency)\n    }\n\n    class OrbiterTaskDependency[\"orbiter.objects.task.OrbiterTaskDependency\"] {\n            task_id: TaskId\n            downstream: TaskId | List[TaskId]\n    }\n\n    class OrbiterTimetable[\"orbiter.objects.timetables.OrbiterTimetable\"] {\n            imports: List[OrbiterRequirements]\n            orbiter_includes: Set[OrbiterIncludes]\n            **kwargs: dict\n    }\n    click OrbiterTimetable href \"#orbiter.objects.timetables.OrbiterTimetable\" \"OrbiterTimetable Documentation\"\n\n    class OrbiterConnection[\"orbiter.objects.connection.OrbiterConnection\"] {\n            conn_id: str\n            conn_type: str\n            **kwargs\n    }\n\n    class OrbiterEnvVar[\"orbiter.objects.env_var.OrbiterEnvVar\"] {\n            key: str\n            value: str\n    }\n\n    class OrbiterPool[\"orbiter.objects.pool.OrbiterPool\"] {\n            name: str\n            description: str | None\n            slots: int | None\n    }\n\n    class OrbiterRequirement[\"orbiter.objects.requirement.OrbiterRequirement\"] {\n            package: str | None\n            module: str | None\n            names: List[str] | None\n            sys_package: str | None\n    }\n\n    class OrbiterVariable[\"orbiter.objects.variable.OrbiterVariable\"] {\n            key: str\n            value: str\n    }
"},{"location":"objects/dags/#orbiter.objects.dag.OrbiterDAG","title":"orbiter.objects.dag.OrbiterDAG","text":"

Represents an Airflow DAG, with its tasks and dependencies.

Renders to a .py file in the /dags folder

Parameters:

Name Type Description file_path str

File path of the DAG, relative to the /dags folder (filepath=my_dag.py would render to dags/my_dag.py)

dag_id str

The dag_id. Must be unique and snake_case. Good practice is to set dag_id == file_path

schedule str | OrbiterTimetable, optional

The schedule for the DAG. Defaults to None (only runs when manually triggered)

catchup bool, optional

Whether to catchup runs from the start_date to now, on first run. Defaults to False

start_date DateTime, optional

The start date for the DAG. Defaults to Unix Epoch

tags List[str], optional

Tags for the DAG, used for sorting and filtering in the Airflow UI

default_args Dict[str, Any], optional

Default arguments for any tasks in the DAG

params Dict[str, Any], optional

Params for the DAG

doc_md str, optional

Documentation for the DAG with markdown support

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/dags/#orbiter.objects.dag.OrbiterDAG.add_tasks","title":"add_tasks","text":"
add_tasks(\n    tasks: (\n        OrbiterOperator\n        | OrbiterTaskGroup\n        | Iterable[OrbiterOperator | OrbiterTaskGroup]\n    ),\n) -> \"OrbiterDAG\"\n

Add one or more OrbiterOperators to the DAG

>>> from orbiter.objects.operators.empty import OrbiterEmptyOperator\n>>> OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks(OrbiterEmptyOperator(task_id=\"bar\")).tasks\n{'bar': bar_task = EmptyOperator(task_id='bar')}\n\n>>> OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks([OrbiterEmptyOperator(task_id=\"bar\")]).tasks\n{'bar': bar_task = EmptyOperator(task_id='bar')}\n

Tip

Validation requires a OrbiterTaskGroup, OrbiterOperator (or subclass), or list of either to be passed

>>> # noinspection PyTypeChecker\n... OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks(\"bar\")\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n... OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks([\"bar\"])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description tasks OrbiterOperator | OrbiterTaskGroup | Iterable[OrbiterOperator | OrbiterTaskGroup]

List of OrbiterOperator, or OrbiterTaskGroup or subclass

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/dag.py
def add_tasks(\n    self,\n    tasks: (\n        OrbiterOperator\n        | OrbiterTaskGroup\n        | Iterable[OrbiterOperator | OrbiterTaskGroup]\n    ),\n) -> \"OrbiterDAG\":\n    \"\"\"\n    Add one or more [`OrbiterOperators`][orbiter.objects.task.OrbiterOperator] to the DAG\n\n    ```pycon\n    >>> from orbiter.objects.operators.empty import OrbiterEmptyOperator\n    >>> OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks(OrbiterEmptyOperator(task_id=\"bar\")).tasks\n    {'bar': bar_task = EmptyOperator(task_id='bar')}\n\n    >>> OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks([OrbiterEmptyOperator(task_id=\"bar\")]).tasks\n    {'bar': bar_task = EmptyOperator(task_id='bar')}\n\n    ```\n\n    !!! tip\n\n        Validation requires a `OrbiterTaskGroup`, `OrbiterOperator` (or subclass), or list of either to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks(\"bar\")\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        ... OrbiterDAG(file_path=\"\", dag_id=\"foo\").add_tasks([\"bar\"])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param tasks: List of [OrbiterOperator][orbiter.objects.task.OrbiterOperator], or OrbiterTaskGroup or subclass\n    :type tasks: OrbiterOperator | OrbiterTaskGroup | Iterable[OrbiterOperator | OrbiterTaskGroup]\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    if (\n        isinstance(tasks, OrbiterOperator)\n        or isinstance(tasks, OrbiterTaskGroup)\n        or issubclass(type(tasks), OrbiterOperator)\n    ):\n        tasks = [tasks]\n\n    for task in tasks:\n        try:\n            task_id = getattr(task, \"task_id\", None) or getattr(\n                task, \"task_group_id\"\n            )\n        except AttributeError:\n            raise AttributeError(\n                f\"Task {task} does not have a task_id or task_group_id attribute\"\n            )\n        self.tasks[task_id] = task\n    return self\n
"},{"location":"objects/dags/#timetables","title":"Timetables","text":""},{"location":"objects/dags/#orbiter.objects.timetables.OrbiterTimetable","title":"orbiter.objects.timetables.OrbiterTimetable","text":"

An Airflow Timetable reference.

Utilizes OrbiterInclude to add a file to a /plugins folder to register the timetable.

Parameters:

Name Type Description **kwargs

any other kwargs to provide to Timetable

"},{"location":"objects/dags/#orbiter.objects.timetables.multi_cron_timetable","title":"orbiter.objects.timetables.multi_cron_timetable","text":""},{"location":"objects/dags/#orbiter.objects.timetables.multi_cron_timetable.OrbiterMultiCronTimetable","title":"orbiter.objects.timetables.multi_cron_timetable.OrbiterMultiCronTimetable","text":"

An Airflow Timetable that can be supplied with multiple cron strings.

>>> OrbiterMultiCronTimetable(cron_defs=[\"*/5 * * * *\", \"*/7 * * * *\"])\nMultiCronTimetable(cron_defs=['*/5 * * * *', '*/7 * * * *'])\n

Parameters:

Name Type Description cron_defs List[str]

A list of cron strings

timezone str

The timezone to use for the timetable

period_length int

The length of the period

period_unit str

The unit of the period

"},{"location":"objects/project/","title":"Project","text":"

An OrbiterProject holds everything necessary to render an Airflow Project. This is generated by a TranslationRuleset.translate_fn.

"},{"location":"objects/project/#diagram","title":"Diagram","text":"
classDiagram\n    direction LR\n\n    OrbiterProject --> \"many\" OrbiterConnection\n    OrbiterProject --> \"many\" OrbiterDAG\n    OrbiterProject --> \"many\" OrbiterEnvVar\n    OrbiterProject --> \"many\" OrbiterInclude\n    OrbiterProject --> \"many\" OrbiterPool\n    OrbiterProject --> \"many\" OrbiterRequirement\n    OrbiterProject --> \"many\" OrbiterVariable\n    class OrbiterProject[\"orbiter.objects.project.OrbiterProject\"] {\n            connections: Dict[str, OrbiterConnection]\n            dags: Dict[str, OrbiterDAG]\n            env_vars: Dict[str, OrbiterEnvVar]\n            includes: Dict[str, OrbiterInclude]\n            pools: Dict[str, OrbiterPool]\n            requirements: Set[OrbiterRequirement]\n            variables: Dict[str, OrbiterVariable]\n    }\n    click OrbiterProject href \"#orbiter.objects.project.OrbiterProject\" \"OrbiterProject Documentation\"\n\n    class OrbiterConnection[\"orbiter.objects.connection.OrbiterConnection\"] {\n            conn_id: str\n            conn_type: str\n            **kwargs\n    }\n    click OrbiterConnection href \"#orbiter.objects.connection.OrbiterConnection\" \"OrbiterConnection Documentation\"\n\n    OrbiterDAG --> \"many\" OrbiterInclude\n    OrbiterDAG --> \"many\" OrbiterConnection\n    OrbiterDAG --> \"many\" OrbiterEnvVar\n    OrbiterDAG --> \"many\" OrbiterPool\n    OrbiterDAG --> \"many\" OrbiterRequirement\n    OrbiterDAG --> \"many\" OrbiterVariable\n    class OrbiterDAG[\"orbiter.objects.dag.OrbiterDAG\"] {\n            imports: List[OrbiterRequirement]\n            file_path: str\n            dag_id: str\n            schedule: str | OrbiterTimetable | None\n            catchup: bool\n            start_date: DateTime\n            tags: List[str]\n            default_args: Dict[str, Any]\n            params: Dict[str, Any]\n            doc_md: str | None\n            tasks: Dict[str, OrbiterOperator]\n            orbiter_kwargs: dict\n            orbiter_conns: Set[OrbiterConnection]\n            orbiter_vars: Set[OrbiterVariable]\n            orbiter_env_vars: Set[OrbiterEnvVar]\n            orbiter_includes: Set[OrbiterInclude]\n    }\n\n    class OrbiterEnvVar[\"orbiter.objects.env_var.OrbiterEnvVar\"] {\n            key: str\n            value: str\n    }\n    click OrbiterEnvVar href \"#orbiter.objects.env_var.OrbiterEnvVar\" \"OrbiterEnvVar Documentation\"\n\n    class OrbiterInclude[\"orbiter.objects.include.OrbiterInclude\"] {\n            filepath: str\n            contents: str\n    }\n    click OrbiterInclude href \"#orbiter.objects.include.OrbiterInclude\" \"OrbiterInclude Documentation\"\n\n    class OrbiterPool[\"orbiter.objects.pool.OrbiterPool\"] {\n            name: str\n            description: str | None\n            slots: int | None\n    }\n    click OrbiterPool href \"#orbiter.objects.pool.OrbiterPool\" \"OrbiterPool Documentation\"\n\n    class OrbiterRequirement[\"orbiter.objects.requirement.OrbiterRequirement\"] {\n            package: str | None\n            module: str | None\n            names: List[str] | None\n            sys_package: str | None\n    }\n    click OrbiterRequirement href \"#orbiter.objects.requirement.OrbiterRequirement\" \"OrbiterRequirement Documentation\"\n\n    class OrbiterVariable[\"orbiter.objects.variable.OrbiterVariable\"] {\n            key: str\n            value: str\n    }\n    click OrbiterVariable href \"#orbiter.objects.variable.OrbiterVariable\" \"OrbiterVariable Documentation\"\n
"},{"location":"objects/project/#orbiter.objects.connection.OrbiterConnection","title":"orbiter.objects.connection.OrbiterConnection","text":"

An Airflow Connection, rendered to an airflow_settings.yaml file.

See also other Connection documentation.

>>> OrbiterConnection(\n...     conn_id=\"my_conn_id\", conn_type=\"mysql\", host=\"localhost\", port=3306, login=\"root\"\n... ).render()\n{'conn_id': 'my_conn_id', 'conn_type': 'mysql', 'conn_host': 'localhost', 'conn_port': 3306, 'conn_login': 'root'}\n

Note

Use the utility conn_id function to generate both an OrbiterConnection and connection property for an operator

from orbiter.objects import conn_id\n\nOrbiterTask(\n    ... ,\n    **conn_id(\"my_conn_id\", conn_type=\"mysql\"),\n)\n

Parameters:

Name Type Description conn_id str

The ID of the connection

conn_type str, optional

The type of the connection, always lowercase. Defaults to 'generic'

**kwargs

Additional properties for the connection

"},{"location":"objects/project/#orbiter.objects.env_var.OrbiterEnvVar","title":"orbiter.objects.env_var.OrbiterEnvVar","text":"

Represents an Environmental Variable, renders to a line in .env file

>>> OrbiterEnvVar(key=\"foo\", value=\"bar\").render()\n'foo=bar'\n

Parameters:

Name Type Description key str

The key of the environment variable

value str

The value of the environment variable

"},{"location":"objects/project/#orbiter.objects.include.OrbiterInclude","title":"orbiter.objects.include.OrbiterInclude","text":"

Represents an included file in an /include directory

Parameters:

Name Type Description filepath str

The relative path (from the output directory) to write the file to

contents str

The contents of the file

"},{"location":"objects/project/#orbiter.objects.pool.OrbiterPool","title":"orbiter.objects.pool.OrbiterPool","text":"

An Airflow Pool, rendered to an airflow_settings.yaml file.

>>> OrbiterPool(name=\"foo\", description=\"bar\", slots=5).render()\n{'pool_name': 'foo', 'pool_description': 'bar', 'pool_slot': 5}\n

Note

Use the utility pool function to easily generate both an OrbiterPool and pool property for an operator

from orbiter.objects import pool\n\nOrbiterTask(\n    ... ,\n    **pool(\"my_pool\"),\n)\n

Parameters:

Name Type Description name str

The name of the pool

description str, optional

The description of the pool

slots int, optional

The number of slots in the pool. Defaults to 128

"},{"location":"objects/project/#orbiter.objects.requirement.OrbiterRequirement","title":"orbiter.objects.requirement.OrbiterRequirement","text":"

A requirement for a project (e.g. apache-airflow-providers-google), and it's representation in the DAG file.

Renders via the DAG File (as an import statement), requirements.txt, and packages.txt

Tip

If a given requirement has multiple packages required, it can be defined as multiple OrbiterRequirement objects.

Example:

OrbiterTask(\n    ...,\n    imports=[\n        OrbiterRequirement(package=\"apache-airflow-providers-google\", ...),\n        OrbiterRequirement(package=\"bigquery\", sys_package=\"mysql\", ...),\n    ],\n)\n

Parameters:

Name Type Description package str, optional

e.g. \"apache-airflow-providers-google\"

module str, optional

e.g. \"airflow.providers.google.cloud.operators.bigquery\", defaults to None

names List[str], optional

e.g. [\"BigQueryCreateEmptyDatasetOperator\"], defaults to []

sys_package Set[str], optional

e.g. \"mysql\" - represents a Debian system package

"},{"location":"objects/project/#orbiter.objects.variable.OrbiterVariable","title":"orbiter.objects.variable.OrbiterVariable","text":"

An Airflow Variable, rendered to an airflow_settings.yaml file.

>>> OrbiterVariable(key=\"foo\", value=\"bar\").render()\n{'variable_value': 'bar', 'variable_name': 'foo'}\n

Parameters:

Name Type Description key str

The key of the variable

value str

The value of the variable

"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject","title":"orbiter.objects.project.OrbiterProject","text":"
OrbiterProject()\n

Holds everything necessary to render an Airflow Project. This is generated by a TranslationRuleset.translate_fn.

Tip

They can be added together

>>> OrbiterProject() + OrbiterProject()\nOrbiterProject(dags=[], requirements=[], pools=[], connections=[], variables=[], env_vars=[])\n

And compared

>>> OrbiterProject() == OrbiterProject()\nTrue\n

Parameters:

Name Type Description connections Dict[str, OrbiterConnection]

A dictionary of OrbiterConnections

dags Dict[str, OrbiterDAG]

A dictionary of OrbiterDAGs

env_vars Dict[str, OrbiterEnvVar]

A dictionary of OrbiterEnvVars

includes Dict[str, OrbiterInclude]

A dictionary of OrbiterIncludes

pools Dict[str, OrbiterPool]

A dictionary of OrbiterPools

requirements Set[OrbiterRequirement]

A set of OrbiterRequirements

variables Dict[str, OrbiterVariable]

A dictionary of OrbiterVariables

Source code in orbiter/objects/project.py
def __init__(self):\n    self.dags: Dict[str, OrbiterDAG] = dict()\n    self.requirements: Set[OrbiterRequirement] = set()\n    self.pools: Dict[str, OrbiterPool] = dict()\n    self.connections: Dict[str, OrbiterConnection] = dict()\n    self.variables: Dict[str, OrbiterVariable] = dict()\n    self.env_vars: Dict[str, OrbiterEnvVar] = dict()\n    self.includes: Dict[str, OrbiterInclude] = dict()\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_connections","title":"add_connections","text":"
add_connections(\n    connections: (\n        OrbiterConnection | Iterable[OrbiterConnection]\n    ),\n) -> \"OrbiterProject\"\n

Add OrbiterConnections to the Project or override an existing connection with new properties

>>> OrbiterProject().add_connections(OrbiterConnection(conn_id='foo')).connections\n{'foo': OrbiterConnection(conn_id=foo, conn_type=generic)}\n\n>>> OrbiterProject().add_connections(\n...     [OrbiterConnection(conn_id='foo'), OrbiterConnection(conn_id='bar')]\n... ).connections\n{'foo': OrbiterConnection(conn_id=foo, conn_type=generic), 'bar': OrbiterConnection(conn_id=bar, conn_type=generic)}\n

Tip

Validation requires an OrbiterConnection to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_connections('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n>>> OrbiterProject().add_connections(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description connections OrbiterConnection | Iterable[OrbiterConnection]

List of OrbiterConnections

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_connections(\n    self, connections: OrbiterConnection | Iterable[OrbiterConnection]\n) -> \"OrbiterProject\":\n    \"\"\"Add [`OrbiterConnections`][orbiter.objects.connection.OrbiterConnection] to the Project\n    or override an existing connection with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_connections(OrbiterConnection(conn_id='foo')).connections\n    {'foo': OrbiterConnection(conn_id=foo, conn_type=generic)}\n\n    >>> OrbiterProject().add_connections(\n    ...     [OrbiterConnection(conn_id='foo'), OrbiterConnection(conn_id='bar')]\n    ... ).connections\n    {'foo': OrbiterConnection(conn_id=foo, conn_type=generic), 'bar': OrbiterConnection(conn_id=bar, conn_type=generic)}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterConnection` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_connections('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        >>> OrbiterProject().add_connections(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n\n    :param connections: List of [`OrbiterConnections`][orbiter.objects.connection.OrbiterConnection]\n    :type connections: List[OrbiterConnection] | OrbiterConnection\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"  # noqa: E501\n    for connection in (\n        [connections] if isinstance(connections, OrbiterConnection) else connections\n    ):\n        self.connections[connection.conn_id] = connection\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_dags","title":"add_dags","text":"
add_dags(\n    dags: OrbiterDAG | Iterable[OrbiterDAG],\n) -> \"OrbiterProject\"\n

Add OrbiterDAGs (and any OrbiterRequirements, OrbiterConns, OrbiterVars, OrbiterPools, OrbiterEnvVars, etc.) to the Project.

>>> OrbiterProject().add_dags(OrbiterDAG(dag_id='foo', file_path=\"\")).dags['foo'].repr()\n'OrbiterDAG(dag_id=foo, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)'\n\n>>> dags = OrbiterProject().add_dags(\n...     [OrbiterDAG(dag_id='foo', file_path=\"\"), OrbiterDAG(dag_id='bar', file_path=\"\")]\n... ).dags; dags['foo'].repr(), dags['bar'].repr()\n... # doctest: +NORMALIZE_WHITESPACE\n('OrbiterDAG(dag_id=foo, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)', 'OrbiterDAG(dag_id=bar, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)')\n\n>>> # An example adding a little of everything, including deeply nested things\n... from orbiter.objects.operators.bash import OrbiterBashOperator\n>>> from orbiter.objects.timetables.multi_cron_timetable import OrbiterMultiCronTimetable\n>>> from orbiter.objects.callbacks.smtp import OrbiterSmtpNotifierCallback\n>>> OrbiterProject().add_dags(OrbiterDAG(\n...     dag_id='foo', file_path=\"\",\n...     orbiter_env_vars={OrbiterEnvVar(key=\"foo\", value=\"bar\")},\n...     orbiter_includes={OrbiterInclude(filepath='foo.txt', contents=\"Hello, World!\")},\n...     schedule=OrbiterMultiCronTimetable(cron_defs=[\"0 */5 * * *\", \"0 */3 * * *\"]),\n...     tasks={'foo': OrbiterTaskGroup(task_group_id=\"foo\",\n...         tasks=[OrbiterBashOperator(\n...             task_id='foo', bash_command='echo \"Hello, World!\"',\n...             orbiter_pool=OrbiterPool(name='foo', slots=1),\n...             orbiter_vars={OrbiterVariable(key='foo', value='bar')},\n...             orbiter_conns={OrbiterConnection(conn_id='foo')},\n...             orbiter_env_vars={OrbiterEnvVar(key='foo', value='bar')},\n...             on_success_callback=OrbiterSmtpNotifierCallback(\n...                 to=\"foo@bar.com\",\n...                 smtp_conn_id=\"SMTP\",\n...                 orbiter_conns={OrbiterConnection(conn_id=\"SMTP\", conn_type=\"smtp\")}\n...             )\n...         )]\n...     )}\n... ))\n... # doctest: +NORMALIZE_WHITESPACE\nOrbiterProject(dags=[foo],\nrequirements=[OrbiterRequirements(names=[DAG], package=apache-airflow, module=airflow, sys_package=None),\nOrbiterRequirements(names=[BashOperator], package=apache-airflow, module=airflow.operators.bash, sys_package=None),\nOrbiterRequirements(names=[send_smtp_notification], package=apache-airflow-providers-smtp, module=airflow.providers.smtp.notifications.smtp, sys_package=None),\nOrbiterRequirements(names=[TaskGroup], package=apache-airflow, module=airflow.utils.task_group, sys_package=None),\nOrbiterRequirements(names=[MultiCronTimetable], package=croniter, module=multi_cron_timetable, sys_package=None),\nOrbiterRequirements(names=[DateTime,Timezone], package=pendulum, module=pendulum, sys_package=None)],\npools=['foo'],\nconnections=['SMTP', 'foo'],\nvariables=['foo'],\nenv_vars=['foo'])\n

Tip

Validation requires an OrbiterDAG to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_dags('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n>>> OrbiterProject().add_dags(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description dags OrbiterDAG | Iterable[OrbiterDAG]

List of OrbiterDAGs

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_dags(self, dags: OrbiterDAG | Iterable[OrbiterDAG]) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterDAGs][orbiter.objects.dag.OrbiterDAG]\n    (and any [OrbiterRequirements][orbiter.objects.requirement.OrbiterRequirement],\n    [OrbiterConns][orbiter.objects.connection.OrbiterConnection],\n    [OrbiterVars][orbiter.objects.variable.OrbiterVariable],\n    [OrbiterPools][orbiter.objects.pool.OrbiterPool],\n    [OrbiterEnvVars][orbiter.objects.env_var.OrbiterEnvVar], etc.)\n    to the Project.\n\n    ```pycon\n    >>> OrbiterProject().add_dags(OrbiterDAG(dag_id='foo', file_path=\"\")).dags['foo'].repr()\n    'OrbiterDAG(dag_id=foo, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)'\n\n    >>> dags = OrbiterProject().add_dags(\n    ...     [OrbiterDAG(dag_id='foo', file_path=\"\"), OrbiterDAG(dag_id='bar', file_path=\"\")]\n    ... ).dags; dags['foo'].repr(), dags['bar'].repr()\n    ... # doctest: +NORMALIZE_WHITESPACE\n    ('OrbiterDAG(dag_id=foo, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)', 'OrbiterDAG(dag_id=bar, schedule=None, start_date=1970-01-01 00:00:00, catchup=False)')\n\n    >>> # An example adding a little of everything, including deeply nested things\n    ... from orbiter.objects.operators.bash import OrbiterBashOperator\n    >>> from orbiter.objects.timetables.multi_cron_timetable import OrbiterMultiCronTimetable\n    >>> from orbiter.objects.callbacks.smtp import OrbiterSmtpNotifierCallback\n    >>> OrbiterProject().add_dags(OrbiterDAG(\n    ...     dag_id='foo', file_path=\"\",\n    ...     orbiter_env_vars={OrbiterEnvVar(key=\"foo\", value=\"bar\")},\n    ...     orbiter_includes={OrbiterInclude(filepath='foo.txt', contents=\"Hello, World!\")},\n    ...     schedule=OrbiterMultiCronTimetable(cron_defs=[\"0 */5 * * *\", \"0 */3 * * *\"]),\n    ...     tasks={'foo': OrbiterTaskGroup(task_group_id=\"foo\",\n    ...         tasks=[OrbiterBashOperator(\n    ...             task_id='foo', bash_command='echo \"Hello, World!\"',\n    ...             orbiter_pool=OrbiterPool(name='foo', slots=1),\n    ...             orbiter_vars={OrbiterVariable(key='foo', value='bar')},\n    ...             orbiter_conns={OrbiterConnection(conn_id='foo')},\n    ...             orbiter_env_vars={OrbiterEnvVar(key='foo', value='bar')},\n    ...             on_success_callback=OrbiterSmtpNotifierCallback(\n    ...                 to=\"foo@bar.com\",\n    ...                 smtp_conn_id=\"SMTP\",\n    ...                 orbiter_conns={OrbiterConnection(conn_id=\"SMTP\", conn_type=\"smtp\")}\n    ...             )\n    ...         )]\n    ...     )}\n    ... ))\n    ... # doctest: +NORMALIZE_WHITESPACE\n    OrbiterProject(dags=[foo],\n    requirements=[OrbiterRequirements(names=[DAG], package=apache-airflow, module=airflow, sys_package=None),\n    OrbiterRequirements(names=[BashOperator], package=apache-airflow, module=airflow.operators.bash, sys_package=None),\n    OrbiterRequirements(names=[send_smtp_notification], package=apache-airflow-providers-smtp, module=airflow.providers.smtp.notifications.smtp, sys_package=None),\n    OrbiterRequirements(names=[TaskGroup], package=apache-airflow, module=airflow.utils.task_group, sys_package=None),\n    OrbiterRequirements(names=[MultiCronTimetable], package=croniter, module=multi_cron_timetable, sys_package=None),\n    OrbiterRequirements(names=[DateTime,Timezone], package=pendulum, module=pendulum, sys_package=None)],\n    pools=['foo'],\n    connections=['SMTP', 'foo'],\n    variables=['foo'],\n    env_vars=['foo'])\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterDAG` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_dags('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        >>> OrbiterProject().add_dags(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n\n    :param dags: List of [OrbiterDAGs][orbiter.objects.dag.OrbiterDAG]\n    :type dags: List[OrbiterDAG] | OrbiterDAG\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"  # noqa: E501\n\n    # noinspection t\n    def _add_recursively(\n        things: Iterable[\n            OrbiterOperator | OrbiterTaskGroup | OrbiterCallback | OrbiterTimetable\n        ],\n    ):\n        for thing in things:\n            if isinstance(thing, str):\n                continue\n            if hasattr(thing, \"orbiter_pool\") and (pool := thing.orbiter_pool):\n                self.add_pools(pool)\n            if hasattr(thing, \"orbiter_conns\") and (conns := thing.orbiter_conns):\n                self.add_connections(conns)\n            if hasattr(thing, \"orbiter_vars\") and (variables := thing.orbiter_vars):\n                self.add_variables(variables)\n            if hasattr(thing, \"orbiter_env_vars\") and (\n                env_vars := thing.orbiter_env_vars\n            ):\n                self.add_env_vars(env_vars)\n            if hasattr(thing, \"orbiter_includes\") and (\n                includes := thing.orbiter_includes\n            ):\n                self.add_includes(includes)\n            if hasattr(thing, \"imports\") and (imports := thing.imports):\n                self.add_requirements(imports)\n            if isinstance(thing, OrbiterTaskGroup) and (tasks := thing.tasks):\n                _add_recursively(tasks)\n\n            # find callbacks in any 'model extra' or attributes named\n            # \"on_success_callback\" or \"on_failure_callback\"\n            if (\n                hasattr(thing, \"__dict__\")\n                and hasattr(thing, \"model_extra\")\n                and len(\n                    (\n                        callbacks := {\n                            k: v\n                            for k, v in (\n                                (thing.__dict__ or dict())\n                                | (thing.model_extra or dict())\n                            ).items()\n                            if k in (\"on_success_callback\", \"on_failure_callback\")\n                            and issubclass(type(v), OrbiterCallback)\n                        }\n                    )\n                )\n            ):\n                _add_recursively(callbacks.values())\n\n    for dag in [dags] if isinstance(dags, OrbiterDAG) else dags:\n        dag_id = dag.dag_id\n\n        # Add or update the DAG\n        if dag_id in self.dags:\n            self.dags[dag_id] += dag\n        else:\n            self.dags[dag_id] = dag\n\n        # Add imports to the project\n        self.add_requirements(dag.imports)\n\n        # Add anything that might be in the tasks of the DAG - such as imports, Connections, etc\n        _add_recursively((dag.tasks or {}).values())\n\n        # Add anything that might be in the `dag.schedule` - such as Includes, Timetables, Connections, etc\n        _add_recursively([dag.schedule])\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_env_vars","title":"add_env_vars","text":"
add_env_vars(\n    env_vars: OrbiterEnvVar | Iterable[OrbiterEnvVar],\n) -> \"OrbiterProject\"\n

Add OrbiterEnvVars to the Project or override an existing env var with new properties

>>> OrbiterProject().add_env_vars(OrbiterEnvVar(key=\"foo\", value=\"bar\")).env_vars\n{'foo': OrbiterEnvVar(key='foo', value='bar')}\n\n>>> OrbiterProject().add_env_vars([OrbiterEnvVar(key=\"foo\", value=\"bar\")]).env_vars\n{'foo': OrbiterEnvVar(key='foo', value='bar')}\n

Tip

Validation requires an OrbiterEnvVar to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_env_vars('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n\n>>> # noinspection PyTypeChecker\n... OrbiterProject().add_env_vars(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description env_vars OrbiterEnvVar | Iterable[OrbiterEnvVar]

List of OrbiterEnvVar

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_env_vars(\n    self, env_vars: OrbiterEnvVar | Iterable[OrbiterEnvVar]\n) -> \"OrbiterProject\":\n    \"\"\"\n    Add [OrbiterEnvVars][orbiter.objects.env_var.OrbiterEnvVar] to the Project\n    or override an existing env var with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_env_vars(OrbiterEnvVar(key=\"foo\", value=\"bar\")).env_vars\n    {'foo': OrbiterEnvVar(key='foo', value='bar')}\n\n    >>> OrbiterProject().add_env_vars([OrbiterEnvVar(key=\"foo\", value=\"bar\")]).env_vars\n    {'foo': OrbiterEnvVar(key='foo', value='bar')}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterEnvVar` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_env_vars('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_env_vars(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n\n    :param env_vars: List of [OrbiterEnvVar][orbiter.objects.env_var.OrbiterEnvVar]\n    :type env_vars: List[OrbiterEnvVar] | OrbiterEnvVar\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for env_var in [env_vars] if isinstance(env_vars, OrbiterEnvVar) else env_vars:\n        self.env_vars[env_var.key] = env_var\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_includes","title":"add_includes","text":"
add_includes(\n    includes: OrbiterInclude | Iterable[OrbiterInclude],\n) -> \"OrbiterProject\"\n

Add OrbiterIncludes to the Project or override an existing OrbiterInclude with new properties

>>> OrbiterProject().add_includes(OrbiterInclude(filepath=\"foo\", contents=\"bar\")).includes\n{'foo': OrbiterInclude(filepath='foo', contents='bar')}\n\n>>> OrbiterProject().add_includes([OrbiterInclude(filepath=\"foo\", contents=\"bar\")]).includes\n{'foo': OrbiterInclude(filepath='foo', contents='bar')}\n

Tip

Validation requires an OrbiterInclude to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_includes('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n\n>>> # noinspection PyTypeChecker\n... OrbiterProject().add_includes(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description includes OrbiterInclude | Iterable[OrbiterInclude]

List of OrbiterIncludes

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_includes(\n    self, includes: OrbiterInclude | Iterable[OrbiterInclude]\n) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterIncludes][orbiter.objects.include.OrbiterInclude] to the Project\n    or override an existing [OrbiterInclude][orbiter.objects.include.OrbiterInclude] with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_includes(OrbiterInclude(filepath=\"foo\", contents=\"bar\")).includes\n    {'foo': OrbiterInclude(filepath='foo', contents='bar')}\n\n    >>> OrbiterProject().add_includes([OrbiterInclude(filepath=\"foo\", contents=\"bar\")]).includes\n    {'foo': OrbiterInclude(filepath='foo', contents='bar')}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterInclude` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_includes('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_includes(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param includes: List of [OrbiterIncludes][orbiter.objects.include.OrbiterInclude]\n    :type includes: List[OrbiterInclude]\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for include in [includes] if isinstance(includes, OrbiterInclude) else includes:\n        self.includes[include.filepath] = include\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_pools","title":"add_pools","text":"
add_pools(\n    pools: OrbiterPool | Iterable[OrbiterPool],\n) -> \"OrbiterProject\"\n

Add OrbiterPool to the Project or override existing pools with new properties

>>> OrbiterProject().add_pools(OrbiterPool(name=\"foo\", slots=1)).pools\n{'foo': OrbiterPool(name='foo', description='', slots=1)}\n\n>>> ( OrbiterProject()\n...     .add_pools([OrbiterPool(name=\"foo\", slots=1)])\n...     .add_pools([OrbiterPool(name=\"foo\", slots=2)])\n...     .pools\n... )\n{'foo': OrbiterPool(name='foo', description='', slots=2)}\n

Tip

Validation requires an OrbiterPool to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_pools('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n... OrbiterProject().add_pools(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description pools OrbiterPool | Iterable[OrbiterPool]

List of OrbiterPools

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_pools(self, pools: OrbiterPool | Iterable[OrbiterPool]) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterPool][orbiter.objects.pool.OrbiterPool] to the Project\n    or override existing pools with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_pools(OrbiterPool(name=\"foo\", slots=1)).pools\n    {'foo': OrbiterPool(name='foo', description='', slots=1)}\n\n    >>> ( OrbiterProject()\n    ...     .add_pools([OrbiterPool(name=\"foo\", slots=1)])\n    ...     .add_pools([OrbiterPool(name=\"foo\", slots=2)])\n    ...     .pools\n    ... )\n    {'foo': OrbiterPool(name='foo', description='', slots=2)}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterPool` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_pools('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_pools(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param pools: List of [OrbiterPools][orbiter.objects.pool.OrbiterPool]\n    :type pools: List[OrbiterPool] | OrbiterPool\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for pool in [pools] if isinstance(pools, OrbiterPool) else pools:\n        if pool.name in self.pools:\n            self.pools[pool.name] += pool\n        else:\n            self.pools[pool.name] = pool\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_requirements","title":"add_requirements","text":"
add_requirements(\n    requirements: (\n        OrbiterRequirement | Iterable[OrbiterRequirement]\n    ),\n) -> \"OrbiterProject\"\n

Add OrbiterRequirements to the Project or override an existing requirement with new properties

>>> OrbiterProject().add_requirements(\n...    OrbiterRequirement(package=\"apache-airflow\", names=['foo'], module='bar'),\n... ).requirements\n{OrbiterRequirements(names=[foo], package=apache-airflow, module=bar, sys_package=None)}\n\n>>> OrbiterProject().add_requirements(\n...    [OrbiterRequirement(package=\"apache-airflow\", names=['foo'], module='bar')],\n... ).requirements\n{OrbiterRequirements(names=[foo], package=apache-airflow, module=bar, sys_package=None)}\n

Tip

Validation requires an OrbiterRequirement to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_requirements('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n>>> OrbiterProject().add_requirements(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description requirements OrbiterRequirement | Iterable[OrbiterRequirement]

List of OrbiterRequirements

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_requirements(\n    self, requirements: OrbiterRequirement | Iterable[OrbiterRequirement]\n) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterRequirements][orbiter.objects.requirement.OrbiterRequirement] to the Project\n    or override an existing requirement with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_requirements(\n    ...    OrbiterRequirement(package=\"apache-airflow\", names=['foo'], module='bar'),\n    ... ).requirements\n    {OrbiterRequirements(names=[foo], package=apache-airflow, module=bar, sys_package=None)}\n\n    >>> OrbiterProject().add_requirements(\n    ...    [OrbiterRequirement(package=\"apache-airflow\", names=['foo'], module='bar')],\n    ... ).requirements\n    {OrbiterRequirements(names=[foo], package=apache-airflow, module=bar, sys_package=None)}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterRequirement` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_requirements('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        >>> OrbiterProject().add_requirements(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param requirements: List of [OrbiterRequirements][orbiter.objects.requirement.OrbiterRequirement]\n    :type requirements: List[OrbiterRequirement] | OrbiterRequirement\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for requirement in (\n        [requirements]\n        if isinstance(requirements, OrbiterRequirement)\n        else requirements\n    ):\n        self.requirements.add(requirement)\n    return self\n
"},{"location":"objects/project/#orbiter.objects.project.OrbiterProject.add_variables","title":"add_variables","text":"
add_variables(\n    variables: OrbiterVariable | Iterable[OrbiterVariable],\n) -> \"OrbiterProject\"\n

Add OrbiterVariables to the Project or override an existing variable with new properties

>>> OrbiterProject().add_variables(OrbiterVariable(key=\"foo\", value=\"bar\")).variables\n{'foo': OrbiterVariable(key='foo', value='bar')}\n\n>>> OrbiterProject().add_variables([OrbiterVariable(key=\"foo\", value=\"bar\")]).variables\n{'foo': OrbiterVariable(key='foo', value='bar')}\n

Tip

Validation requires an OrbiterVariable to be passed

>>> # noinspection PyTypeChecker\n... OrbiterProject().add_variables('foo')\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n>>> # noinspection PyTypeChecker\n... OrbiterProject().add_variables(['foo'])\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\npydantic_core._pydantic_core.ValidationError: ...\n

Parameters:

Name Type Description variables OrbiterVariable | Iterable[OrbiterVariable]

List of OrbiterVariable

Returns:

Type Description OrbiterProject

self

Source code in orbiter/objects/project.py
def add_variables(\n    self, variables: OrbiterVariable | Iterable[OrbiterVariable]\n) -> \"OrbiterProject\":\n    \"\"\"Add [OrbiterVariables][orbiter.objects.variable.OrbiterVariable] to the Project\n    or override an existing variable with new properties\n\n    ```pycon\n    >>> OrbiterProject().add_variables(OrbiterVariable(key=\"foo\", value=\"bar\")).variables\n    {'foo': OrbiterVariable(key='foo', value='bar')}\n\n    >>> OrbiterProject().add_variables([OrbiterVariable(key=\"foo\", value=\"bar\")]).variables\n    {'foo': OrbiterVariable(key='foo', value='bar')}\n\n    ```\n\n    !!! tip\n\n        Validation requires an `OrbiterVariable` to be passed\n        ```pycon\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_variables('foo')\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n        >>> # noinspection PyTypeChecker\n        ... OrbiterProject().add_variables(['foo'])\n        ... # doctest: +IGNORE_EXCEPTION_DETAIL\n        Traceback (most recent call last):\n        pydantic_core._pydantic_core.ValidationError: ...\n\n        ```\n    :param variables: List of [OrbiterVariable][orbiter.objects.variable.OrbiterVariable]\n    :type variables: List[OrbiterVariable] | OrbiterVariable\n    :return: self\n    :rtype: OrbiterProject\n    \"\"\"\n    for variable in (\n        [variables] if isinstance(variables, OrbiterVariable) else variables\n    ):\n        self.variables[variable.key] = variable\n    return self\n
"},{"location":"objects/Tasks/","title":"Overview","text":"

Airflow Tasks are units of work. An Operator is a pre-defined task with specific functionality.

Operators can be looked up in the Astronomer Registry.

The easiest way to utilize an operator is to use a subclass of OrbiterOperator (e.g. OrbiterBashOperator).

If an OrbiterOperator subclass doesn't exist for your use case, you can:

1) Utilize OrbiterTask

from orbiter.objects.requirement import OrbiterRequirement\nfrom orbiter.objects.task import OrbiterTask\nfrom orbiter.rules import task_rule\n\n@task_rule\ndef my_rule(val: dict):\n    return OrbiterTask(\n        task_id=\"my_task\",\n        imports=[OrbiterRequirement(\n            package=\"apache-airflow\",\n            module=\"airflow.operators.trigger_dagrun\",\n            names=[\"TriggerDagRunOperator\"],\n        )],\n        ...\n    )\n

2) Create a new subclass of OrbiterOperator (beneficial if you are using it frequently in separate @task_rules)

from orbiter.objects.task import OrbiterOperator\nfrom orbiter.objects.requirement import OrbiterRequirement\nfrom orbiter.rules import task_rule\n\nclass OrbiterTriggerDagRunOperator(OrbiterOperator):\n    # Define the imports required for the operator, and the operator name\n    imports = [\n        OrbiterRequirement(\n            package=\"apache-airflow\",\n            module=\"airflow.operators.trigger_dagrun\",\n            names=[\"TriggerDagRunOperator\"],\n        )\n    ]\n    operator: str = \"PythonOperator\"\n\n    # Add fields should be rendered in the output\n    render_attributes = OrbiterOperator.render_attributes + [\n        ...\n    ]\n\n    # Add the fields that are required for the operator here, with their types\n    # Not all Airflow Operator fields are required, just the ones you will use.\n    trigger_dag_id: str\n    ...\n\n@task_rule\ndef my_rule(val: dict):\n    return OrbiterTriggerDagRunOperator(...)\n

"},{"location":"objects/Tasks/#diagram","title":"Diagram","text":"
classDiagram\n    direction LR\n    OrbiterOperator \"implements\" <|-- OrbiterTask\n    OrbiterOperator --> \"many\" OrbiterCallback\n    class OrbiterOperator[\"orbiter.objects.task.OrbiterOperator\"] {\n            imports: List[OrbiterRequirement]\n            operator: str\n            task_id: str\n            pool: str | None\n            pool_slots: int | None\n            trigger_rule: str | None\n            downstream: Set[OrbiterTaskDependency]\n            add_downstream(str | List[str] | OrbiterTaskDependency)\n    }\n    click OrbiterOperator href \"#orbiter.objects.task.OrbiterOperator\" \"OrbiterOperator Documentation\"\n\n    class OrbiterTask[\"orbiter.objects.task.OrbiterTask\"] {\n        <<OrbiterOperator>>\n            <<OrbiterOperator>>\n            imports: List[OrbiterRequirement]\n            task_id: str\n            **kwargs\n    }\n    click OrbiterTask href \"#orbiter.objects.task.OrbiterTask\" \"OrbiterTask Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterBashOperator\n    class OrbiterBashOperator[\"orbiter.objects.operators.bash.OrbiterBashOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"BashOperator\"\n            task_id: str\n            bash_command: str\n    }\n    click OrbiterBashOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.bash.OrbiterBashOperator\" \"OrbiterBashOperator Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterEmailOperator\n    class OrbiterEmailOperator[\"orbiter.objects.operators.smtp.OrbiterEmailOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"EmailOperator\"\n            task_id: str\n            to: str | list[str]\n            subject: str\n            html_content: str\n            files: list | None\n            conn_id: str\n    }\n    click OrbiterEmailOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.smtp.OrbiterEmailOperator\" \"OrbiterEmailOperator Documentation\"\n\n  OrbiterOperator  \"implements\" <|--  OrbiterEmptyOperator\n    class OrbiterEmptyOperator[\"orbiter.objects.operators.empty.OrbiterEmptyOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"BashOperator\"\n            task_id: str\n    }\n    click OrbiterEmptyOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.empty.OrbiterEmptyOperator\" \"OrbiterEmptyOperator Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterPythonOperator\n    class OrbiterPythonOperator[\"orbiter.objects.operators.python.OrbiterPythonOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"PythonOperator\"\n            task_id: str\n            python_callable: Callable\n            op_args: list | None\n            op_kwargs: dict | None\n    }\n    click OrbiterPythonOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.python.OrbiterPythonOperator\" \"OrbiterPythonOperator Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterSQLExecuteQueryOperator\n    class OrbiterSQLExecuteQueryOperator[\"orbiter.objects.operators.sql.OrbiterSQLExecuteQueryOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"SQLExecuteQueryOperator\"\n            task_id: str\n            conn_id: str\n            sql: str\n    }\n    click OrbiterSQLExecuteQueryOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.sql.OrbiterSQLExecuteQueryOperator\" \"OrbiterSQLExecuteQueryOperator Documentation\"\n\n    OrbiterOperator \"implements\" <|-- OrbiterSSHOperator\n    class OrbiterSSHOperator[\"orbiter.objects.operators.ssh.OrbiterSSHOperator\"] {\n        <<OrbiterOperator>>\n            operator = \"SSHOperator\"\n            task_id: str\n            ssh_conn_id: str\n            command: str\n            environment: Dict[str, str] | None\n    }\n    click OrbiterSSHOperator href \"Operators_and_Callbacks/operators#orbiter.objects.operators.ssh.OrbiterSSHOperator\" \"OrbiterSSHOperator Documentation\"\n\n    class OrbiterCallback[\"orbiter.objects.callbacks.OrbiterCallback\"] {\n            function: str\n    }\n    click OrbiterCallback href \"Operators_and_Callbacks/callbacks#orbiter.objects.callbacks.OrbiterCallback\" \"OrbiterCallback Documentation\"\n\n    OrbiterCallback \"implements\" <|--  OrbiterSmtpNotifierCallback\n    class OrbiterSmtpNotifierCallback[\"orbiter.objects.callbacks.smtp.OrbiterSmtpNotifierCallback\"] {\n        <<OrbiterCallback>>\n            to: str\n            from_email: str\n            smtp_conn_id: str\n            subject: str\n            html_content: str\n            cc: str | Iterable[str]\n    }\n    click OrbiterSmtpNotifierCallback href \"Operators_and_Callbacks/callbacks#orbiter.objects.callbacks.smtp.OrbiterSmtpNotifierCallback\" \"OrbiterSmtpNotifierCallback Documentation\"
"},{"location":"objects/Tasks/#orbiter.objects.task.OrbiterOperator","title":"orbiter.objects.task.OrbiterOperator","text":"

Abstract class representing a Task in Airflow, must be subclassed (such as OrbiterBashOperator)

Instantiation/inheriting:

>>> from orbiter.objects import OrbiterRequirement\n>>> class OrbiterMyOperator(OrbiterOperator):\n...   imports: ImportList = [OrbiterRequirement(package=\"apache-airflow\")]\n...   operator: str = \"MyOperator\"\n\n>>> foo = OrbiterMyOperator(task_id=\"task_id\"); foo\ntask_id_task = MyOperator(task_id='task_id')\n

Adding single downstream tasks:

>>> foo.add_downstream(\"downstream\").downstream\n{task_id_task >> downstream_task}\n

Adding multiple downstream tasks:

>>> sorted(list(foo.add_downstream([\"a\", \"b\"]).downstream))\n[task_id_task >> [a_task, b_task], task_id_task >> downstream_task]\n

Note

Validation - task_id in OrbiterTaskDependency must match this task_id

>>> foo.add_downstream(OrbiterTaskDependency(task_id=\"other\", downstream=\"bar\")).downstream\n... # doctest: +IGNORE_EXCEPTION_DETAIL\nTraceback (most recent call last):\nValueError: Task dependency ... has a different task_id than task_id\n

Parameters:

Name Type Description imports List[OrbiterRequirement]

List of requirements for the operator

task_id str

The task_id for the operator, must be unique and snake_case

trigger_rule str, optional

Conditions under which to start the task (docs)

pool str, optional

Name of the pool to use

pool_slots int, optional

Slots for this task to occupy

operator str, optional

Operator name

downstream Set[OrbiterTaskDependency], optional

Downstream tasks, defaults to set()

**kwargs

Other properties that may be passed to operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/#orbiter.objects.task.OrbiterTaskDependency","title":"orbiter.objects.task.OrbiterTaskDependency","text":"

Represents a task dependency, which is added to either an OrbiterOperator or an OrbiterTaskGroup.

Can take a single downstream task_id

>>> OrbiterTaskDependency(task_id=\"task_id\", downstream=\"downstream\")\ntask_id_task >> downstream_task\n

or a list of downstream task_ids

>>> OrbiterTaskDependency(task_id=\"task_id\", downstream=[\"a\", \"b\"])\ntask_id_task >> [a_task, b_task]\n

Parameters:

Name Type Description task_id str

The task_id for the operator

downstream str | List[str]

downstream tasks

"},{"location":"objects/Tasks/#orbiter.objects.task.OrbiterTask","title":"orbiter.objects.task.OrbiterTask","text":"

A generic Airflow OrbiterOperator that can be instantiated directly.

The operator that is instantiated is inferred from the imports field.

The first *Operator or *Sensor import is used.

View info for specific operators at the Astronomer Registry.

>>> from orbiter.objects.requirement import OrbiterRequirement\n>>> OrbiterTask(task_id=\"foo\", bash_command=\"echo 'hello world'\", other=1, imports=[\n...   OrbiterRequirement(package=\"apache-airflow\", module=\"airflow.operators.bash\", names=[\"BashOperator\"])\n... ])\nfoo_task = BashOperator(task_id='foo', bash_command=\"echo 'hello world'\", other=1)\n\n>>> def foo():\n...   pass\n>>> OrbiterTask(task_id=\"foo\", python_callable=foo, other=1, imports=[\n...   OrbiterRequirement(package=\"apache-airflow\", module=\"airflow.sensors.python\", names=[\"PythonSensor\"])\n... ])\ndef foo():\n    pass\nfoo_task = PythonSensor(task_id='foo', other=1, python_callable=foo)\n

Parameters:

Name Type Description task_id str

The task_id for the operator. Must be unique and snake_case

imports List[OrbiterRequirement]

List of requirements for the operator. The Operator is inferred from first *Operator or *Sensor imported.

**kwargs

Any other keyword arguments to be passed to the operator

"},{"location":"objects/Tasks/#orbiter.objects.task_group.OrbiterTaskGroup","title":"orbiter.objects.task_group.OrbiterTaskGroup","text":"

Represents a TaskGroup in Airflow, which contains multiple tasks

>>> from orbiter.objects.operators.bash import OrbiterBashOperator\n>>> OrbiterTaskGroup(task_group_id=\"foo\", tasks=[\n...   OrbiterBashOperator(task_id=\"b\", bash_command=\"b\"),\n...   OrbiterBashOperator(task_id=\"a\", bash_command=\"a\").add_downstream(\"b\"),\n... ])\nwith TaskGroup(group_id='foo') as foo:\n    b_task = BashOperator(task_id='b', bash_command='b')\n    a_task = BashOperator(task_id='a', bash_command='a')\n    a_task >> b_task\n

Parameters:

Name Type Description task_group_id str

The id of the TaskGroup

tasks List[OrbiterOperator | OrbiterTaskGroup]

The tasks in the TaskGroup

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/callbacks/","title":"Callbacks","text":"

Airflow callback functions are often used to send emails, slack messages, or other notifications when a task fails, succeeds, or is retried. They can also run any general Python function.

"},{"location":"objects/Tasks/Operators_and_Callbacks/callbacks/#orbiter.objects.callbacks.OrbiterCallback","title":"orbiter.objects.callbacks.OrbiterCallback","text":"

Abstract class representing an Airflow callback function, which might be used in DAG.on_failure_callback, or Task.on_success_callback, or etc.

>>> class OrbiterMyCallback(OrbiterCallback):\n...   function: str = \"my_callback\"\n...   foo: str\n...   bar: str\n...   render_attributes: RenderAttributes = [\"foo\", \"bar\"]\n>>> OrbiterMyCallback(foo=\"fop\", bar=\"bop\")\nmy_callback(foo='fop', bar='bop')\n

Parameters:

Name Type Description function str

The name of the function to call

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/callbacks/#orbiter.objects.callbacks.smtp","title":"orbiter.objects.callbacks.smtp","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/callbacks/#orbiter.objects.callbacks.smtp.OrbiterSmtpNotifierCallback","title":"orbiter.objects.callbacks.smtp.OrbiterSmtpNotifierCallback","text":"

An Airflow SMTP Callback (link)

Note

Use smtp_conn_id and reference an SMTP Connection.

You can use the **conn_id(\"SMTP\", conn_type=\"smtp\") utility function to set both properties at once.

>>> [_import] = OrbiterSmtpNotifierCallback(to=\"foo@test.com\").imports; _import\nOrbiterRequirements(names=[send_smtp_notification], package=apache-airflow-providers-smtp, module=airflow.providers.smtp.notifications.smtp, sys_package=None)\n\n>>> OrbiterSmtpNotifierCallback(to=\"foo@test.com\", from_email=\"bar@test.com\", subject=\"Hello\", html_content=\"World\")\nsend_smtp_notification(to='foo@test.com', from_email='bar@test.com', smtp_conn_id='SMTP', subject='Hello', html_content='World')\n

Parameters:

Name Type Description to str | Iterable[str]

The email address to send to

from_email str, optional

The email address to send from

smtp_conn_id str, optional

The connection id to use (Note: use the **conn_id(...) utility function). Defaults to \"SMTP\"

subject str, optional

The subject of the email

html_content str, optional

The content of the email

cc str | Iterable[str], optional

The email address to cc

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/","title":"Operators","text":"

Note

These operators are included and are intended to represent some of the most common Airflow Operators, but not all Airflow Operators.

Additional Operators can be created by subclassing OrbiterOperator or using OrbiterTask directly.

Review the Astronomer Registry to find additional Airflow Operators.

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.bash","title":"orbiter.objects.operators.bash","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.bash.OrbiterBashOperator","title":"orbiter.objects.operators.bash.OrbiterBashOperator","text":"

Bases: OrbiterOperator

An Airflow BashOperator. Used to run shell commands.

>>> OrbiterBashOperator(task_id=\"foo\", bash_command=\"echo 'hello world'\")\nfoo_task = BashOperator(task_id='foo', bash_command=\"echo 'hello world'\")\n

Parameters:

Name Type Description task_id str

The task_id for the operator

bash_command str

The shell command to execute

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.empty","title":"orbiter.objects.operators.empty","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.empty.OrbiterEmptyOperator","title":"orbiter.objects.operators.empty.OrbiterEmptyOperator","text":"

Bases: OrbiterOperator

An Airflow EmptyOperator. Does nothing.

>>> OrbiterEmptyOperator(task_id=\"foo\")\nfoo_task = EmptyOperator(task_id='foo')\n

Parameters:

Name Type Description task_id str

The task_id for the operator

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.python","title":"orbiter.objects.operators.python","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.python.OrbiterPythonOperator","title":"orbiter.objects.operators.python.OrbiterPythonOperator","text":"

Bases: OrbiterOperator

An Airflow PythonOperator. Used to execute any Python Function.

>>> def foo(a, b):\n...    print(a + b)\n>>> OrbiterPythonOperator(task_id=\"foo\", python_callable=foo)\ndef foo(a, b):\n   print(a + b)\nfoo_task = PythonOperator(task_id='foo', python_callable=foo)\n

Parameters:

Name Type Description task_id str

The task_id for the operator

python_callable Callable

The python function to execute

op_args list | None, optional

The arguments to pass to the python function, defaults to None

op_kwargs dict | None, optional

The keyword arguments to pass to the python function, defaults to None

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.smtp","title":"orbiter.objects.operators.smtp","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.smtp.OrbiterEmailOperator","title":"orbiter.objects.operators.smtp.OrbiterEmailOperator","text":"

Bases: OrbiterOperator

An Airflow EmailOperator. Used to send emails.

>>> OrbiterEmailOperator(\n...   task_id=\"foo\", to=\"humans@astronomer.io\", subject=\"Hello\", html_content=\"World!\"\n... )\nfoo_task = EmailOperator(task_id='foo', to='humans@astronomer.io', subject='Hello', html_content='World!', conn_id='SMTP')\n

Parameters:

Name Type Description task_id str

The task_id for the operator

to str | list[str]

The recipient of the email

subject str

The subject of the email

html_content str

The content of the email

files list, optional

The files to attach to the email, defaults to None

conn_id str, optional

The SMTP connection to use. Defaults to \"SMTP\" and sets orbiter_conns property

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.sql","title":"orbiter.objects.operators.sql","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.sql.OrbiterSQLExecuteQueryOperator","title":"orbiter.objects.operators.sql.OrbiterSQLExecuteQueryOperator","text":"

Bases: OrbiterOperator

An Airflow Generic SQL Operator. Used to run SQL against any Database.

>>> OrbiterSQLExecuteQueryOperator(\n...   task_id=\"foo\", conn_id='sql', sql=\"select 1;\"\n... )\nfoo_task = SQLExecuteQueryOperator(task_id='foo', conn_id='sql', sql='select 1;')\n

Parameters:

Name Type Description task_id str

The task_id for the operator

conn_id str

The SQL connection to utilize. (Note: use the **conn_id(...) utility function)

sql str

The SQL to execute

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.ssh","title":"orbiter.objects.operators.ssh","text":""},{"location":"objects/Tasks/Operators_and_Callbacks/operators/#orbiter.objects.operators.ssh.OrbiterSSHOperator","title":"orbiter.objects.operators.ssh.OrbiterSSHOperator","text":"

Bases: OrbiterOperator

An Airflow SSHOperator. Used to run shell commands over SSH.

>>> OrbiterSSHOperator(task_id=\"foo\", ssh_conn_id=\"SSH\", command=\"echo 'hello world'\")\nfoo_task = SSHOperator(task_id='foo', ssh_conn_id='SSH', command=\"echo 'hello world'\")\n

Parameters:

Name Type Description task_id str

The task_id for the operator

ssh_conn_id str

The SSH connection to use. (Note: use the **conn_id(...) utility function)

command str

The command to execute

environment dict, optional

The environment variables to set, defaults to None

**kwargs

Extra arguments to pass to the operator

**OrbiterBase

OrbiterBase inherited properties

"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index f6c3b84..ca04923 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,67 +2,67 @@ https://astronomer.github.io/orbiter/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/cli/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/origins/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/Rules_and_Rulesets/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/Rules_and_Rulesets/rules/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/Rules_and_Rulesets/rulesets/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/Rules_and_Rulesets/template/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/objects/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/objects/dags/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/objects/project/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/objects/Tasks/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/objects/Tasks/Operators_and_Callbacks/callbacks/ - 2024-08-16 + 2024-08-21 daily https://astronomer.github.io/orbiter/objects/Tasks/Operators_and_Callbacks/operators/ - 2024-08-16 + 2024-08-21 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index c4c222142f51813b2db2e60c11de155545ed2370..34909a17efa02039f585c4694d98e57b58f37dd3 100644 GIT binary patch literal 314 zcmV-A0mc3wiwFonX2oU#|8r?{Wo=<_E_iKh0Ns=^Z-X!ph4=gl#Ewl+hqNMOsMNJm zmClhdE;uzldS^=e?`t55m@8Gn_2TEJ=hJs5+4&rhhcJpjXpe&4DJa(GPN8ss^Rs&t#*{*OU*6Pz=mxI--*+=@{_84! M0BOYf-*yQA0Ks9ONdN!< literal 314 zcmV-A0mc3wiwFq7&c0>>|8r?{Wo=<_E_iKh0Ns>9PlPZKh41?-2zOd`W6WxRJ(zek zG3$9#N-fpW!I>iZ?=7&q;cQ|8_R`mv*U2}NwAtaSvC*eOGT`!f%hEVfPC{jz&f}Nk zW3rDA#U|^}lJZdqJIdqQ`}UsmzV8_suNu)31Js;k9S+3LCQsR2Tx_DSh60lsS7t#L zLlp{T1bGApjY6i4ue*{N;DBW)f;{6xbbd3$8>ot1y1PlzeX_j`k+V08$yzwAUQvA% zl`!@@<1=SM;#a0$(@ARjW7)2djXT7|*`PS5T^SX&Aie0f#>p&PjPf8Wixg|Dml M0rpQb{B{Wd0M2=v>;M1&