uranusjr commented on code in PR #74084:
URL: https://github.com/apache/airflow/pull/74084#discussion_r4236858200


##########
task-sdk/src/airflow/sdk/importers/yaml_importer/models.py:
##########
@@ -0,0 +1,362 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Pydantic models for the YAML/JSON DAG format.
+
+These are the single source of truth for the format; they validate an authored
+document (rich Pydantic errors) and back the published JSON Schema. Only the
+format skeleton and value grammar only is described; operator ``with:`` args,
+Dag- and task-level attributes pass through untyped because the Python
+constructors validate those at import time.
+"""
+
+from __future__ import annotations
+
+import itertools
+from typing import Annotated, Any
+
+from pydantic import (
+    AliasChoices,
+    BaseModel,
+    ConfigDict,
+    Discriminator,
+    Field,
+    RootModel,
+    Tag,
+    TypeAdapter,
+    model_validator,
+)
+
+XCOM_KEYS = ("$x", "$xcom")
+TEMPLATE_KEYS = ("$t", "$template")
+CONST_KEY = "$const"
+
+# Keys deferred to a later edition...
+DAG_CALLBACK_KEYS = {"on_success_callback", "on_failure_callback", 
"sla_miss_callback"}
+TASK_CALLBACK_KEYS = {
+    "on_success_callback",
+    "on_failure_callback",
+    "on_retry_callback",
+    "on_execute_callback",
+    "on_skipped_callback",
+    "sla_miss_callback",
+}
+TASK_ASSET_IO_KEYS = {"inlets", "outlets"}
+
+# Structural keys the format owns (everything else is pass-through to Python).
+_DAG_STRUCTURAL = {"$schema", "dag_id", "schedule", "templates", "tasks"}
+
+
+class XComTarget(BaseModel):
+    """The object form of an XCom reference."""
+
+    task: str
+    key: str | None = None
+
+    model_config = ConfigDict(extra="forbid")
+
+
+class XComRef(BaseModel):
+    """An upstream task's XCom output."""
+
+    target: str | XComTarget = Field(
+        serialization_alias="$x",
+        validation_alias=AliasChoices(*XCOM_KEYS),
+    )
+
+    model_config = ConfigDict(extra="forbid", populate_by_name=True)
+
+
+class TemplateRef(BaseModel):
+    """A Jinja template."""
+
+    source: str = Field(serialization_alias="$t", 
validation_alias=AliasChoices(*TEMPLATE_KEYS))
+
+    model_config = ConfigDict(extra="forbid", populate_by_name=True)
+
+
+class ConstRef(BaseModel):
+    """Force a literal; the value is taken verbatim."""
+
+    value: Any = Field(serialization_alias="$const", 
validation_alias=AliasChoices(CONST_KEY))
+
+    model_config = ConfigDict(extra="forbid", populate_by_name=True)
+
+
+def _value_discriminator(v: Any) -> str:
+    """
+    Route a raw value to a marker branch, or to ``literal``.
+
+    A reserved ``$``-marker is recognised only as the sole key of an object, so
+    a multi-key dict that merely contains ``$x`` is a plain literal dict.
+    """
+    if isinstance(v, dict) and len(v) == 1:
+        (key,) = v
+        if key in XCOM_KEYS:
+            return "xcom"
+        if key in TEMPLATE_KEYS:
+            return "template"
+        if key == CONST_KEY:
+            return "const"
+    # Already-parsed marker instances (revalidation) route to themselves.
+    if isinstance(v, XComRef):
+        return "xcom"
+    if isinstance(v, TemplateRef):
+        return "template"
+    if isinstance(v, ConstRef):
+        return "const"
+    return "literal"
+
+
+class _Literal(RootModel[Any]):
+    """
+    A literal value.
+
+    Containers recurse so nested markers are still resolved; scalars pass
+    through.
+    """
+
+    root: Any
+
+    @model_validator(mode="before")
+    @classmethod
+    def _recurse(cls, v: Any) -> Any:
+        if isinstance(v, dict):
+            return {k: _ValueAdapter.validate_python(item) for k, item in 
v.items()}
+        if isinstance(v, list):
+            return [_ValueAdapter.validate_python(item) for item in v]
+        return v
+
+
+Value = Annotated[
+    Annotated[XComRef, Tag("xcom")]
+    | Annotated[TemplateRef, Tag("template")]
+    | Annotated[ConstRef, Tag("const")]
+    | Annotated[_Literal, Tag("literal")],
+    Discriminator(_value_discriminator),
+]
+
+_ValueAdapter: TypeAdapter[Any] = TypeAdapter(Value)
+
+
+class _TaskBase(BaseModel):
+    """
+    Fields common to both ``use:`` and ``run:`` task kinds.
+
+    Unknown keys are BaseOperator arguments (pass-through).
+    """
+
+    id_: str = Field(alias="id")
+    needs: list[str] = Field(default_factory=list)
+    extends: list[str] = Field(default_factory=list)
+    with_: dict[str, Value] = Field(default_factory=dict, alias="with")
+
+    model_config = ConfigDict(extra="allow", populate_by_name=True)
+
+    @model_validator(mode="before")
+    @classmethod
+    def _check_task(cls, data: Any) -> Any:
+        if isinstance(data, dict):
+            for k in TASK_ASSET_IO_KEYS:
+                if k in data:
+                    raise ValueError(f"{k!r} not yet implemented")
+            for k in TASK_CALLBACK_KEYS:
+                if k in data:
+                    raise ValueError(f"{k!r} (callback) not yet implemented")
+            bodies = [k for k in ("uses", "run") if k in data]
+            if len(bodies) != 1:
+                raise ValueError(
+                    f"task {data.get('id')!r} must have exactly one of 'uses' 
or 'run'"
+                    + (f"; got {bodies}" if bodies else "")
+                )
+        return data
+
+
+class OperatorTask(_TaskBase):
+    """
+    A ready-made operator.
+
+    This should have ``uses`` (import path) + ``with``.
+    """
+
+    uses: str
+
+
+class CodeTask(_TaskBase):
+    """
+    A task running custom code.
+
+    This should have ``run`` holding function arguments, and optionally
+    ``queue`` to route it.
+    """
+
+    run: dict[str, Value] = Field(default_factory=dict)
+
+
+def _task_discriminator(v: Any) -> str:
+    if isinstance(v, OperatorTask):
+        return "operator"
+    if isinstance(v, CodeTask):
+        return "code"
+    if isinstance(v, dict) and "uses" in v:
+        return "operator"
+    return "code"
+
+
+Task = Annotated[
+    Annotated[OperatorTask, Tag("operator")] | Annotated[CodeTask, 
Tag("code")],
+    Discriminator(_task_discriminator),
+]
+
+
+class TaskTemplate(BaseModel):
+    """
+    A reusable, partial task fragment merged into any task that ``extends`` it.
+
+    This holds arbitrary keys as-is. All validation is done on the merged task.
+    """
+
+    model_config = ConfigDict(extra="allow")
+
+
+class TimetableSchedule(BaseModel):
+    """A constructed timetable."""
+
+    uses: str
+    with_: dict[str, Value] = Field(default_factory=dict, alias="with")
+
+    model_config = ConfigDict(extra="forbid", populate_by_name=True)
+
+
+Schedule = str | None | TimetableSchedule
+
+
+def _merge_task(base: dict, over: dict) -> dict:
+    """
+    Shallow merge *over* onto *base*.
+
+    Note that ``with`` and ``run`` need to merge one level deep. All other
+    fields simply replace.
+    """
+    out = base.copy()
+    for k, v in over.items():
+        if k in ("with", "run") and isinstance(v, dict) and 
isinstance(out.get(k), dict):
+            merged = dict(out[k])
+            merged.update(v)
+            out[k] = merged
+        else:
+            out[k] = v
+    return out
+
+
+def _expand_template(name: str, templates: dict[str, Any], _seen: tuple = ()) 
-> dict:
+    if name in _seen:
+        raise ValueError(f"template cycle via {name!r}")
+    if name not in templates:

Review Comment:
   I changed the parser to accept `list[str] | str` with a check in later lines.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to