prefect-client 2.20.4__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefect/__init__.py +74 -110
- prefect/_internal/compatibility/deprecated.py +6 -115
- prefect/_internal/compatibility/experimental.py +4 -79
- prefect/_internal/compatibility/migration.py +166 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/api.py +1 -35
- prefect/_internal/concurrency/calls.py +0 -6
- prefect/_internal/concurrency/cancellation.py +0 -3
- prefect/_internal/concurrency/event_loop.py +0 -20
- prefect/_internal/concurrency/inspection.py +3 -3
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/concurrency/services.py +23 -0
- prefect/_internal/concurrency/threads.py +35 -0
- prefect/_internal/concurrency/waiters.py +0 -28
- prefect/_internal/integrations.py +7 -0
- prefect/_internal/pydantic/__init__.py +0 -45
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pydantic/v1_schema.py +21 -22
- prefect/_internal/pydantic/v2_schema.py +0 -2
- prefect/_internal/pydantic/v2_validated_func.py +18 -23
- prefect/_internal/pytz.py +1 -1
- prefect/_internal/retries.py +61 -0
- prefect/_internal/schemas/bases.py +45 -177
- prefect/_internal/schemas/fields.py +1 -43
- prefect/_internal/schemas/validators.py +47 -233
- prefect/agent.py +3 -695
- prefect/artifacts.py +173 -14
- prefect/automations.py +39 -4
- prefect/blocks/abstract.py +1 -1
- prefect/blocks/core.py +405 -153
- prefect/blocks/fields.py +2 -57
- prefect/blocks/notifications.py +43 -28
- prefect/blocks/redis.py +168 -0
- prefect/blocks/system.py +67 -20
- prefect/blocks/webhook.py +2 -9
- prefect/cache_policies.py +239 -0
- prefect/client/__init__.py +4 -0
- prefect/client/base.py +33 -27
- prefect/client/cloud.py +65 -20
- prefect/client/collections.py +1 -1
- prefect/client/orchestration.py +650 -442
- prefect/client/schemas/actions.py +115 -100
- prefect/client/schemas/filters.py +46 -52
- prefect/client/schemas/objects.py +228 -178
- prefect/client/schemas/responses.py +18 -36
- prefect/client/schemas/schedules.py +55 -36
- prefect/client/schemas/sorting.py +2 -0
- prefect/client/subscriptions.py +8 -7
- prefect/client/types/flexible_schedule_list.py +11 -0
- prefect/client/utilities.py +9 -6
- prefect/concurrency/asyncio.py +60 -11
- prefect/concurrency/context.py +24 -0
- prefect/concurrency/events.py +2 -2
- prefect/concurrency/services.py +46 -16
- prefect/concurrency/sync.py +51 -7
- prefect/concurrency/v1/asyncio.py +143 -0
- prefect/concurrency/v1/context.py +27 -0
- prefect/concurrency/v1/events.py +61 -0
- prefect/concurrency/v1/services.py +116 -0
- prefect/concurrency/v1/sync.py +92 -0
- prefect/context.py +246 -149
- prefect/deployments/__init__.py +33 -18
- prefect/deployments/base.py +10 -15
- prefect/deployments/deployments.py +2 -1048
- prefect/deployments/flow_runs.py +178 -0
- prefect/deployments/runner.py +72 -173
- prefect/deployments/schedules.py +31 -25
- prefect/deployments/steps/__init__.py +0 -1
- prefect/deployments/steps/core.py +7 -0
- prefect/deployments/steps/pull.py +15 -21
- prefect/deployments/steps/utility.py +2 -1
- prefect/docker/__init__.py +20 -0
- prefect/docker/docker_image.py +82 -0
- prefect/engine.py +15 -2475
- prefect/events/actions.py +17 -23
- prefect/events/cli/automations.py +20 -7
- prefect/events/clients.py +142 -80
- prefect/events/filters.py +14 -18
- prefect/events/related.py +74 -75
- prefect/events/schemas/__init__.py +0 -5
- prefect/events/schemas/automations.py +55 -46
- prefect/events/schemas/deployment_triggers.py +7 -197
- prefect/events/schemas/events.py +46 -65
- prefect/events/schemas/labelling.py +10 -14
- prefect/events/utilities.py +4 -5
- prefect/events/worker.py +23 -8
- prefect/exceptions.py +15 -0
- prefect/filesystems.py +30 -529
- prefect/flow_engine.py +827 -0
- prefect/flow_runs.py +379 -7
- prefect/flows.py +470 -360
- prefect/futures.py +382 -331
- prefect/infrastructure/__init__.py +5 -26
- prefect/infrastructure/base.py +3 -320
- prefect/infrastructure/provisioners/__init__.py +5 -3
- prefect/infrastructure/provisioners/cloud_run.py +13 -8
- prefect/infrastructure/provisioners/container_instance.py +14 -9
- prefect/infrastructure/provisioners/ecs.py +10 -8
- prefect/infrastructure/provisioners/modal.py +8 -5
- prefect/input/__init__.py +4 -0
- prefect/input/actions.py +2 -4
- prefect/input/run_input.py +9 -9
- prefect/logging/formatters.py +2 -4
- prefect/logging/handlers.py +9 -14
- prefect/logging/loggers.py +5 -5
- prefect/main.py +72 -0
- prefect/plugins.py +2 -64
- prefect/profiles.toml +16 -2
- prefect/records/__init__.py +1 -0
- prefect/records/base.py +223 -0
- prefect/records/filesystem.py +207 -0
- prefect/records/memory.py +178 -0
- prefect/records/result_store.py +64 -0
- prefect/results.py +577 -504
- prefect/runner/runner.py +117 -47
- prefect/runner/server.py +32 -34
- prefect/runner/storage.py +3 -12
- prefect/runner/submit.py +2 -10
- prefect/runner/utils.py +2 -2
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +40 -5
- prefect/runtime/task_run.py +1 -0
- prefect/serializers.py +28 -39
- prefect/server/api/collections_data/views/aggregate-worker-metadata.json +5 -14
- prefect/settings.py +209 -332
- prefect/states.py +160 -63
- prefect/task_engine.py +1478 -57
- prefect/task_runners.py +383 -287
- prefect/task_runs.py +240 -0
- prefect/task_worker.py +463 -0
- prefect/tasks.py +684 -374
- prefect/transactions.py +410 -0
- prefect/types/__init__.py +72 -86
- prefect/types/entrypoint.py +13 -0
- prefect/utilities/annotations.py +4 -3
- prefect/utilities/asyncutils.py +227 -148
- prefect/utilities/callables.py +137 -45
- prefect/utilities/collections.py +134 -86
- prefect/utilities/dispatch.py +27 -14
- prefect/utilities/dockerutils.py +11 -4
- prefect/utilities/engine.py +186 -32
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +26 -27
- prefect/utilities/pydantic.py +128 -38
- prefect/utilities/schema_tools/hydration.py +18 -1
- prefect/utilities/schema_tools/validation.py +30 -0
- prefect/utilities/services.py +35 -9
- prefect/utilities/templating.py +12 -2
- prefect/utilities/timeout.py +20 -5
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +78 -59
- prefect/workers/__init__.py +0 -1
- prefect/workers/base.py +237 -244
- prefect/workers/block.py +5 -226
- prefect/workers/cloud.py +6 -0
- prefect/workers/process.py +265 -12
- prefect/workers/server.py +29 -11
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/METADATA +28 -24
- prefect_client-3.0.0.dist-info/RECORD +201 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/WHEEL +1 -1
- prefect/_internal/pydantic/_base_model.py +0 -51
- prefect/_internal/pydantic/_compat.py +0 -82
- prefect/_internal/pydantic/_flags.py +0 -20
- prefect/_internal/pydantic/_types.py +0 -8
- prefect/_internal/pydantic/utilities/config_dict.py +0 -72
- prefect/_internal/pydantic/utilities/field_validator.py +0 -150
- prefect/_internal/pydantic/utilities/model_construct.py +0 -56
- prefect/_internal/pydantic/utilities/model_copy.py +0 -55
- prefect/_internal/pydantic/utilities/model_dump.py +0 -136
- prefect/_internal/pydantic/utilities/model_dump_json.py +0 -112
- prefect/_internal/pydantic/utilities/model_fields.py +0 -50
- prefect/_internal/pydantic/utilities/model_fields_set.py +0 -29
- prefect/_internal/pydantic/utilities/model_json_schema.py +0 -82
- prefect/_internal/pydantic/utilities/model_rebuild.py +0 -80
- prefect/_internal/pydantic/utilities/model_validate.py +0 -75
- prefect/_internal/pydantic/utilities/model_validate_json.py +0 -68
- prefect/_internal/pydantic/utilities/model_validator.py +0 -87
- prefect/_internal/pydantic/utilities/type_adapter.py +0 -71
- prefect/_vendor/fastapi/__init__.py +0 -25
- prefect/_vendor/fastapi/applications.py +0 -946
- prefect/_vendor/fastapi/background.py +0 -3
- prefect/_vendor/fastapi/concurrency.py +0 -44
- prefect/_vendor/fastapi/datastructures.py +0 -58
- prefect/_vendor/fastapi/dependencies/__init__.py +0 -0
- prefect/_vendor/fastapi/dependencies/models.py +0 -64
- prefect/_vendor/fastapi/dependencies/utils.py +0 -877
- prefect/_vendor/fastapi/encoders.py +0 -177
- prefect/_vendor/fastapi/exception_handlers.py +0 -40
- prefect/_vendor/fastapi/exceptions.py +0 -46
- prefect/_vendor/fastapi/logger.py +0 -3
- prefect/_vendor/fastapi/middleware/__init__.py +0 -1
- prefect/_vendor/fastapi/middleware/asyncexitstack.py +0 -25
- prefect/_vendor/fastapi/middleware/cors.py +0 -3
- prefect/_vendor/fastapi/middleware/gzip.py +0 -3
- prefect/_vendor/fastapi/middleware/httpsredirect.py +0 -3
- prefect/_vendor/fastapi/middleware/trustedhost.py +0 -3
- prefect/_vendor/fastapi/middleware/wsgi.py +0 -3
- prefect/_vendor/fastapi/openapi/__init__.py +0 -0
- prefect/_vendor/fastapi/openapi/constants.py +0 -2
- prefect/_vendor/fastapi/openapi/docs.py +0 -203
- prefect/_vendor/fastapi/openapi/models.py +0 -480
- prefect/_vendor/fastapi/openapi/utils.py +0 -485
- prefect/_vendor/fastapi/param_functions.py +0 -340
- prefect/_vendor/fastapi/params.py +0 -453
- prefect/_vendor/fastapi/py.typed +0 -0
- prefect/_vendor/fastapi/requests.py +0 -4
- prefect/_vendor/fastapi/responses.py +0 -40
- prefect/_vendor/fastapi/routing.py +0 -1331
- prefect/_vendor/fastapi/security/__init__.py +0 -15
- prefect/_vendor/fastapi/security/api_key.py +0 -98
- prefect/_vendor/fastapi/security/base.py +0 -6
- prefect/_vendor/fastapi/security/http.py +0 -172
- prefect/_vendor/fastapi/security/oauth2.py +0 -227
- prefect/_vendor/fastapi/security/open_id_connect_url.py +0 -34
- prefect/_vendor/fastapi/security/utils.py +0 -10
- prefect/_vendor/fastapi/staticfiles.py +0 -1
- prefect/_vendor/fastapi/templating.py +0 -3
- prefect/_vendor/fastapi/testclient.py +0 -1
- prefect/_vendor/fastapi/types.py +0 -3
- prefect/_vendor/fastapi/utils.py +0 -235
- prefect/_vendor/fastapi/websockets.py +0 -7
- prefect/_vendor/starlette/__init__.py +0 -1
- prefect/_vendor/starlette/_compat.py +0 -28
- prefect/_vendor/starlette/_exception_handler.py +0 -80
- prefect/_vendor/starlette/_utils.py +0 -88
- prefect/_vendor/starlette/applications.py +0 -261
- prefect/_vendor/starlette/authentication.py +0 -159
- prefect/_vendor/starlette/background.py +0 -43
- prefect/_vendor/starlette/concurrency.py +0 -59
- prefect/_vendor/starlette/config.py +0 -151
- prefect/_vendor/starlette/convertors.py +0 -87
- prefect/_vendor/starlette/datastructures.py +0 -707
- prefect/_vendor/starlette/endpoints.py +0 -130
- prefect/_vendor/starlette/exceptions.py +0 -60
- prefect/_vendor/starlette/formparsers.py +0 -276
- prefect/_vendor/starlette/middleware/__init__.py +0 -17
- prefect/_vendor/starlette/middleware/authentication.py +0 -52
- prefect/_vendor/starlette/middleware/base.py +0 -220
- prefect/_vendor/starlette/middleware/cors.py +0 -176
- prefect/_vendor/starlette/middleware/errors.py +0 -265
- prefect/_vendor/starlette/middleware/exceptions.py +0 -74
- prefect/_vendor/starlette/middleware/gzip.py +0 -113
- prefect/_vendor/starlette/middleware/httpsredirect.py +0 -19
- prefect/_vendor/starlette/middleware/sessions.py +0 -82
- prefect/_vendor/starlette/middleware/trustedhost.py +0 -64
- prefect/_vendor/starlette/middleware/wsgi.py +0 -147
- prefect/_vendor/starlette/py.typed +0 -0
- prefect/_vendor/starlette/requests.py +0 -328
- prefect/_vendor/starlette/responses.py +0 -347
- prefect/_vendor/starlette/routing.py +0 -933
- prefect/_vendor/starlette/schemas.py +0 -154
- prefect/_vendor/starlette/staticfiles.py +0 -248
- prefect/_vendor/starlette/status.py +0 -199
- prefect/_vendor/starlette/templating.py +0 -231
- prefect/_vendor/starlette/testclient.py +0 -804
- prefect/_vendor/starlette/types.py +0 -30
- prefect/_vendor/starlette/websockets.py +0 -193
- prefect/blocks/kubernetes.py +0 -119
- prefect/deprecated/__init__.py +0 -0
- prefect/deprecated/data_documents.py +0 -350
- prefect/deprecated/packaging/__init__.py +0 -12
- prefect/deprecated/packaging/base.py +0 -96
- prefect/deprecated/packaging/docker.py +0 -146
- prefect/deprecated/packaging/file.py +0 -92
- prefect/deprecated/packaging/orion.py +0 -80
- prefect/deprecated/packaging/serializers.py +0 -171
- prefect/events/instrument.py +0 -135
- prefect/infrastructure/container.py +0 -824
- prefect/infrastructure/kubernetes.py +0 -920
- prefect/infrastructure/process.py +0 -289
- prefect/manifests.py +0 -20
- prefect/new_flow_engine.py +0 -449
- prefect/new_task_engine.py +0 -423
- prefect/pydantic/__init__.py +0 -76
- prefect/pydantic/main.py +0 -39
- prefect/software/__init__.py +0 -2
- prefect/software/base.py +0 -50
- prefect/software/conda.py +0 -199
- prefect/software/pip.py +0 -122
- prefect/software/python.py +0 -52
- prefect/task_server.py +0 -322
- prefect_client-2.20.4.dist-info/RECORD +0 -294
- /prefect/{_internal/pydantic/utilities → client/types}/__init__.py +0 -0
- /prefect/{_vendor → concurrency/v1}/__init__.py +0 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/LICENSE +0 -0
- {prefect_client-2.20.4.dist-info → prefect_client-3.0.0.dist-info}/top_level.txt +0 -0
prefect/software/conda.py
DELETED
@@ -1,199 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import re
|
3
|
-
import subprocess
|
4
|
-
import sys
|
5
|
-
from pathlib import Path
|
6
|
-
from typing import List, Type
|
7
|
-
|
8
|
-
import yaml
|
9
|
-
|
10
|
-
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
11
|
-
|
12
|
-
if HAS_PYDANTIC_V2:
|
13
|
-
from pydantic.v1 import Field, validate_arguments
|
14
|
-
else:
|
15
|
-
from pydantic import Field, validate_arguments
|
16
|
-
from typing_extensions import Self
|
17
|
-
|
18
|
-
from prefect.software.base import (
|
19
|
-
Requirement,
|
20
|
-
pop_requirement_by_name,
|
21
|
-
remove_duplicate_requirements,
|
22
|
-
)
|
23
|
-
from prefect.software.pip import current_environment_requirements
|
24
|
-
from prefect.software.python import PythonEnvironment
|
25
|
-
from prefect.utilities.collections import listrepr
|
26
|
-
|
27
|
-
# Capture each component of a conda requirement string.
|
28
|
-
#
|
29
|
-
# <name><version_specifier><version><build_specifier><build>
|
30
|
-
#
|
31
|
-
# The specification of these requirements is more relaxed than pip requirements.
|
32
|
-
# Notably, the version specifier is typically a single equal sign and a build can be
|
33
|
-
# included after the version.
|
34
|
-
#
|
35
|
-
# For example, the requirement "requests=2.25.1=pyhd3eb1b0_0" matches as:
|
36
|
-
# name: "requests"
|
37
|
-
# version_specifier: "="
|
38
|
-
# version: "2.25.1"
|
39
|
-
# build_specifier: "="
|
40
|
-
# build: "pyhd3eb1b0_0"
|
41
|
-
#
|
42
|
-
# Regex components:
|
43
|
-
# name (required): any combination of numbers, letters, or dashes
|
44
|
-
# version_specifier (optional): one or more of `>`, `<`, and `=`
|
45
|
-
# version (optional): any combination of numbers, letters, or periods; not valid
|
46
|
-
# unless grouped with a version specifier.
|
47
|
-
# build_specifier (optional): just `=`.
|
48
|
-
# build (optional): any combination of numbers, letters, or underscores; not valid
|
49
|
-
# unless grouped with a build specifier.
|
50
|
-
|
51
|
-
CONDA_REQUIREMENT = re.compile(
|
52
|
-
r"^((?P<channel>[0-9A-Za-z\_\-\/]+)::)?"
|
53
|
-
r"(?P<name>[0-9A-Za-z_\-\.]+)"
|
54
|
-
r"((?P<version_specifier>[>=<]+)(?P<version>[0-9a-zA-Z\.]+))?"
|
55
|
-
r"((?P<build_specifier>=)(?P<build>[0-9A-Za-z\_\[\]\=]+))?$"
|
56
|
-
)
|
57
|
-
|
58
|
-
|
59
|
-
class CondaRequirement(Requirement):
|
60
|
-
"""
|
61
|
-
A parsed requirement for installation with conda.
|
62
|
-
"""
|
63
|
-
|
64
|
-
def __init__(self, requirement_string: str):
|
65
|
-
self._requirement_string = requirement_string
|
66
|
-
|
67
|
-
parsed = CONDA_REQUIREMENT.match(requirement_string)
|
68
|
-
if parsed is None:
|
69
|
-
raise ValueError(
|
70
|
-
f"Invalid requirement {requirement_string!r}: could not be parsed."
|
71
|
-
)
|
72
|
-
self._parts = parsed.groupdict()
|
73
|
-
|
74
|
-
self.name = self._parts["name"]
|
75
|
-
self.version_specifier = self._parts["version_specifier"]
|
76
|
-
self.version = self._parts["version"]
|
77
|
-
self.build_specifier = self._parts["build_specifier"]
|
78
|
-
self.build = self._parts["build"]
|
79
|
-
|
80
|
-
def __str__(self) -> str:
|
81
|
-
return self._requirement_string
|
82
|
-
|
83
|
-
|
84
|
-
class CondaError(RuntimeError):
|
85
|
-
"""
|
86
|
-
Raised if an error occurs in conda.
|
87
|
-
"""
|
88
|
-
|
89
|
-
|
90
|
-
def current_environment_conda_requirements(
|
91
|
-
include_builds: bool = False, explicit_only: bool = True
|
92
|
-
) -> List[CondaRequirement]:
|
93
|
-
"""
|
94
|
-
Return conda requirements by exporting the current environment.
|
95
|
-
|
96
|
-
Skips any pip requirements included in the export. Only requirements that are
|
97
|
-
managed by conda are returned.
|
98
|
-
"""
|
99
|
-
command = ["conda", "env", "export", "--json"]
|
100
|
-
|
101
|
-
if not include_builds:
|
102
|
-
command.append("--no-builds")
|
103
|
-
if explicit_only:
|
104
|
-
command.append("--from-history")
|
105
|
-
|
106
|
-
process = subprocess.run(command, capture_output=True)
|
107
|
-
parsed = json.loads(process.stdout)
|
108
|
-
if "error" in parsed:
|
109
|
-
raise CondaError(
|
110
|
-
"Encountered an exception while exporting the conda environment: "
|
111
|
-
+ parsed["error"]
|
112
|
-
)
|
113
|
-
|
114
|
-
# If no dependencies are given, this field will not be present
|
115
|
-
dependencies = parsed.get("dependencies", [])
|
116
|
-
|
117
|
-
# The string check will exclude nested objects like the 'pip' subtree
|
118
|
-
return [CondaRequirement(dep) for dep in dependencies if isinstance(dep, str)]
|
119
|
-
|
120
|
-
|
121
|
-
class CondaEnvironment(PythonEnvironment):
|
122
|
-
conda_requirements: List[CondaRequirement] = Field(default_factory=list)
|
123
|
-
|
124
|
-
@classmethod
|
125
|
-
def from_environment(cls: Type[Self], exclude_nested: bool = False) -> Self:
|
126
|
-
conda_requirements = (
|
127
|
-
current_environment_conda_requirements()
|
128
|
-
if "conda" in sys.executable
|
129
|
-
else []
|
130
|
-
)
|
131
|
-
pip_requirements = remove_duplicate_requirements(
|
132
|
-
conda_requirements,
|
133
|
-
current_environment_requirements(
|
134
|
-
exclude_nested=exclude_nested, on_uninstallable_requirement="warn"
|
135
|
-
),
|
136
|
-
)
|
137
|
-
python_requirement = pop_requirement_by_name(conda_requirements, "python")
|
138
|
-
python_version = python_requirement.version if python_requirement else None
|
139
|
-
|
140
|
-
return cls(
|
141
|
-
pip_requirements=pip_requirements,
|
142
|
-
conda_requirements=conda_requirements,
|
143
|
-
python_version=python_version,
|
144
|
-
)
|
145
|
-
|
146
|
-
@classmethod
|
147
|
-
@validate_arguments
|
148
|
-
def from_file(cls: Type[Self], path: Path) -> Self:
|
149
|
-
parsed = yaml.safe_load(path.read_text())
|
150
|
-
|
151
|
-
# If no dependencies are given, this field will not be present
|
152
|
-
dependencies = parsed.get("dependencies", [])
|
153
|
-
|
154
|
-
# The string check will exclude nested objects like the 'pip' subtree
|
155
|
-
conda_requirements = [
|
156
|
-
CondaRequirement(dep) for dep in dependencies if isinstance(dep, str)
|
157
|
-
]
|
158
|
-
|
159
|
-
python_requirement = pop_requirement_by_name(conda_requirements, "python")
|
160
|
-
python_version = python_requirement.version if python_requirement else None
|
161
|
-
|
162
|
-
other_requirements = {}
|
163
|
-
|
164
|
-
# Parse nested requirements. We only support 'pip' for now but we'll check for
|
165
|
-
# others
|
166
|
-
for subtree in [dep for dep in dependencies if isinstance(dep, dict)]:
|
167
|
-
key = list(subtree.keys())[0]
|
168
|
-
|
169
|
-
if key in other_requirements:
|
170
|
-
raise ValueError(
|
171
|
-
"Invalid conda requirements specification. "
|
172
|
-
f"Found duplicate key {key!r}."
|
173
|
-
)
|
174
|
-
|
175
|
-
other_requirements[key] = subtree[key]
|
176
|
-
|
177
|
-
pip_requirements = other_requirements.pop("pip", [])
|
178
|
-
|
179
|
-
if other_requirements:
|
180
|
-
raise ValueError(
|
181
|
-
"Found unsupported requirements types in file: "
|
182
|
-
f"{listrepr(other_requirements.keys(), ', ')}"
|
183
|
-
)
|
184
|
-
|
185
|
-
return cls(
|
186
|
-
conda_requirements=conda_requirements,
|
187
|
-
pip_requirements=pip_requirements,
|
188
|
-
python_version=python_version,
|
189
|
-
)
|
190
|
-
|
191
|
-
def install_commands(self) -> List[str]:
|
192
|
-
pip_install_commands = super().install_commands()
|
193
|
-
|
194
|
-
if not self.conda_requirements:
|
195
|
-
return pip_install_commands
|
196
|
-
|
197
|
-
return [
|
198
|
-
["conda", "install", *(str(req) for req in self.conda_requirements)]
|
199
|
-
] + pip_install_commands
|
prefect/software/pip.py
DELETED
@@ -1,122 +0,0 @@
|
|
1
|
-
import site
|
2
|
-
import warnings
|
3
|
-
from pathlib import Path
|
4
|
-
from typing import Dict, List, Type, Union
|
5
|
-
|
6
|
-
import packaging.requirements
|
7
|
-
import packaging.version
|
8
|
-
from typing_extensions import Literal, Self
|
9
|
-
|
10
|
-
from prefect.software.base import Requirement
|
11
|
-
from prefect.utilities.compat import importlib_metadata
|
12
|
-
|
13
|
-
|
14
|
-
class PipRequirement(Requirement, packaging.requirements.Requirement):
|
15
|
-
"""
|
16
|
-
A parsed Python requirement.
|
17
|
-
|
18
|
-
An extension for `packaging.version.Requirement` with Pydantic support.
|
19
|
-
"""
|
20
|
-
|
21
|
-
@classmethod
|
22
|
-
def from_distribution(
|
23
|
-
cls: Type[Self], dist: "importlib_metadata.Distribution"
|
24
|
-
) -> Self:
|
25
|
-
"""
|
26
|
-
Convert a Python distribution object into a requirement
|
27
|
-
"""
|
28
|
-
if _is_editable_install(dist):
|
29
|
-
raise ValueError(
|
30
|
-
f"Distribution {dist!r} is an editable installation and cannot be "
|
31
|
-
"used as a requirement."
|
32
|
-
)
|
33
|
-
|
34
|
-
return cls.validate(
|
35
|
-
packaging.requirements.Requirement(
|
36
|
-
f"{dist.metadata['name']}=={dist.version}"
|
37
|
-
)
|
38
|
-
)
|
39
|
-
|
40
|
-
|
41
|
-
def _get_installed_distributions() -> Dict[str, "importlib_metadata.Distribution"]:
|
42
|
-
return {dist.name: dist for dist in importlib_metadata.distributions()}
|
43
|
-
|
44
|
-
|
45
|
-
def _is_child_path(path: Union[Path, str], parent: Union[Path, str]) -> bool:
|
46
|
-
return Path(parent).resolve() in Path(path).resolve().parents
|
47
|
-
|
48
|
-
|
49
|
-
def _is_same_path(path: Union[Path, str], other: Union[Path, str]) -> bool:
|
50
|
-
return Path(path).resolve() == Path(other).resolve()
|
51
|
-
|
52
|
-
|
53
|
-
def _is_editable_install(dist: "importlib_metadata.Distribution") -> bool:
|
54
|
-
"""
|
55
|
-
Determine if a distribution is an 'editable' install by scanning if it is present
|
56
|
-
in a site-packages directory or not; if not, we presume it is editable.
|
57
|
-
"""
|
58
|
-
site_packages = site.getsitepackages() + [site.getusersitepackages()]
|
59
|
-
|
60
|
-
dist_location = dist.locate_file("")
|
61
|
-
for site_package_dir in site_packages:
|
62
|
-
if _is_same_path(dist_location, site_package_dir) or _is_child_path(
|
63
|
-
dist_location, site_package_dir
|
64
|
-
):
|
65
|
-
return False
|
66
|
-
|
67
|
-
return True
|
68
|
-
|
69
|
-
|
70
|
-
def _remove_distributions_required_by_others(
|
71
|
-
dists: Dict[str, "importlib_metadata.Distribution"],
|
72
|
-
) -> Dict[str, "importlib_metadata.Distribution"]:
|
73
|
-
# Collect all child requirements
|
74
|
-
child_requirement_names = set()
|
75
|
-
for dist in dists.values():
|
76
|
-
child_requirement_names.update(dist.requires or [])
|
77
|
-
|
78
|
-
return {
|
79
|
-
name: dist
|
80
|
-
for name, dist in dists.items()
|
81
|
-
if name not in child_requirement_names
|
82
|
-
}
|
83
|
-
|
84
|
-
|
85
|
-
def current_environment_requirements(
|
86
|
-
exclude_nested: bool = False,
|
87
|
-
on_uninstallable_requirement: Literal["ignore", "warn", "raise"] = "warn",
|
88
|
-
) -> List[PipRequirement]:
|
89
|
-
dists = _get_installed_distributions()
|
90
|
-
if exclude_nested:
|
91
|
-
dists = _remove_distributions_required_by_others(dists)
|
92
|
-
|
93
|
-
requirements = []
|
94
|
-
uninstallable_msgs = []
|
95
|
-
for dist in dists.values():
|
96
|
-
if _is_editable_install(dist):
|
97
|
-
uninstallable_msgs.append(
|
98
|
-
f"- {dist.name}: This distribution is an editable installation."
|
99
|
-
)
|
100
|
-
else:
|
101
|
-
requirements.append(PipRequirement.from_distribution(dist))
|
102
|
-
|
103
|
-
if uninstallable_msgs:
|
104
|
-
message = (
|
105
|
-
"The following requirements will not be installable on another machine:\n"
|
106
|
-
+ "\n".join(uninstallable_msgs)
|
107
|
-
)
|
108
|
-
if on_uninstallable_requirement == "ignore":
|
109
|
-
pass
|
110
|
-
elif on_uninstallable_requirement == "warn":
|
111
|
-
# When warning, include a note that these distributions are excluded
|
112
|
-
warnings.warn(message + "\nThese requirements will be ignored.")
|
113
|
-
elif on_uninstallable_requirement == "raise":
|
114
|
-
raise ValueError(message)
|
115
|
-
else:
|
116
|
-
raise ValueError(
|
117
|
-
"Unknown mode for `on_uninstallable_requirement`: "
|
118
|
-
f"{on_uninstallable_requirement!r}. "
|
119
|
-
"Expected one of: 'ignore', 'warn', 'raise'."
|
120
|
-
)
|
121
|
-
|
122
|
-
return requirements
|
prefect/software/python.py
DELETED
@@ -1,52 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import List, Type
|
3
|
-
|
4
|
-
from prefect._internal.pydantic import HAS_PYDANTIC_V2
|
5
|
-
from prefect._internal.schemas.validators import infer_python_version
|
6
|
-
|
7
|
-
if HAS_PYDANTIC_V2:
|
8
|
-
from pydantic.v1 import BaseModel, Field, validate_arguments, validator
|
9
|
-
else:
|
10
|
-
from pydantic import BaseModel, Field, validate_arguments, validator
|
11
|
-
|
12
|
-
from typing_extensions import Self
|
13
|
-
|
14
|
-
from prefect.software.pip import PipRequirement, current_environment_requirements
|
15
|
-
|
16
|
-
|
17
|
-
class PythonEnvironment(BaseModel):
|
18
|
-
"""
|
19
|
-
A specification for a Python environment.
|
20
|
-
"""
|
21
|
-
|
22
|
-
python_version: str = None
|
23
|
-
pip_requirements: List[PipRequirement] = Field(default_factory=list)
|
24
|
-
|
25
|
-
@validator("python_version", pre=True, always=True)
|
26
|
-
def validate_python_version(cls, value):
|
27
|
-
return infer_python_version(value)
|
28
|
-
|
29
|
-
@classmethod
|
30
|
-
def from_environment(cls: Type[Self], exclude_nested: bool = False) -> Self:
|
31
|
-
"""
|
32
|
-
Generate requirements from the current environment
|
33
|
-
|
34
|
-
Arguments:
|
35
|
-
exclude_nested: If True, only top-level requirements will be included.
|
36
|
-
Defaults to including all requirements.
|
37
|
-
"""
|
38
|
-
pip_requirements = current_environment_requirements(
|
39
|
-
exclude_nested=exclude_nested, on_uninstallable_requirement="warn"
|
40
|
-
)
|
41
|
-
return cls(pip_requirements=pip_requirements)
|
42
|
-
|
43
|
-
@classmethod
|
44
|
-
@validate_arguments
|
45
|
-
def from_file(cls: Type[Self], path: Path) -> Self:
|
46
|
-
return PythonEnvironment(pip_requirements=path.read_text().strip().splitlines())
|
47
|
-
|
48
|
-
def install_commands(self) -> List[List[str]]:
|
49
|
-
if not self.pip_requirements:
|
50
|
-
return []
|
51
|
-
|
52
|
-
return [["pip", "install", *(str(req) for req in self.pip_requirements)]]
|
prefect/task_server.py
DELETED
@@ -1,322 +0,0 @@
|
|
1
|
-
import asyncio
|
2
|
-
import inspect
|
3
|
-
import os
|
4
|
-
import signal
|
5
|
-
import socket
|
6
|
-
import sys
|
7
|
-
from contextlib import AsyncExitStack
|
8
|
-
from functools import partial
|
9
|
-
from typing import List, Optional, Type
|
10
|
-
|
11
|
-
import anyio
|
12
|
-
from exceptiongroup import BaseExceptionGroup # novermin
|
13
|
-
from websockets.exceptions import InvalidStatusCode
|
14
|
-
|
15
|
-
from prefect import Task, get_client
|
16
|
-
from prefect._internal.concurrency.api import create_call, from_sync
|
17
|
-
from prefect.client.schemas.objects import TaskRun
|
18
|
-
from prefect.client.subscriptions import Subscription
|
19
|
-
from prefect.engine import emit_task_run_state_change_event, propose_state
|
20
|
-
from prefect.exceptions import Abort, PrefectHTTPStatusError
|
21
|
-
from prefect.logging.loggers import get_logger
|
22
|
-
from prefect.results import ResultFactory
|
23
|
-
from prefect.settings import (
|
24
|
-
PREFECT_API_URL,
|
25
|
-
PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING,
|
26
|
-
PREFECT_TASK_SCHEDULING_DELETE_FAILED_SUBMISSIONS,
|
27
|
-
)
|
28
|
-
from prefect.states import Pending
|
29
|
-
from prefect.task_engine import submit_autonomous_task_run_to_engine
|
30
|
-
from prefect.task_runners import (
|
31
|
-
BaseTaskRunner,
|
32
|
-
ConcurrentTaskRunner,
|
33
|
-
)
|
34
|
-
from prefect.utilities.asyncutils import asyncnullcontext, sync_compatible
|
35
|
-
from prefect.utilities.processutils import _register_signal
|
36
|
-
|
37
|
-
logger = get_logger("task_server")
|
38
|
-
|
39
|
-
|
40
|
-
class StopTaskServer(Exception):
|
41
|
-
"""Raised when the task server is stopped."""
|
42
|
-
|
43
|
-
pass
|
44
|
-
|
45
|
-
|
46
|
-
def should_try_to_read_parameters(task: Task, task_run: TaskRun) -> bool:
|
47
|
-
"""Determines whether a task run should read parameters from the result factory."""
|
48
|
-
new_enough_state_details = hasattr(
|
49
|
-
task_run.state.state_details, "task_parameters_id"
|
50
|
-
)
|
51
|
-
task_accepts_parameters = bool(inspect.signature(task.fn).parameters)
|
52
|
-
|
53
|
-
return new_enough_state_details and task_accepts_parameters
|
54
|
-
|
55
|
-
|
56
|
-
class TaskServer:
|
57
|
-
"""This class is responsible for serving tasks that may be executed in the background
|
58
|
-
by a task runner via the traditional engine machinery.
|
59
|
-
|
60
|
-
When `start()` is called, the task server will open a websocket connection to a
|
61
|
-
server-side queue of scheduled task runs. When a scheduled task run is found, the
|
62
|
-
scheduled task run is submitted to the engine for execution with a minimal `EngineContext`
|
63
|
-
so that the task run can be governed by orchestration rules.
|
64
|
-
|
65
|
-
Args:
|
66
|
-
- tasks: A list of tasks to serve. These tasks will be submitted to the engine
|
67
|
-
when a scheduled task run is found.
|
68
|
-
- task_runner: The task runner to use for executing the tasks. Defaults to
|
69
|
-
`ConcurrentTaskRunner`.
|
70
|
-
"""
|
71
|
-
|
72
|
-
def __init__(
|
73
|
-
self,
|
74
|
-
*tasks: Task,
|
75
|
-
task_runner: Optional[Type[BaseTaskRunner]] = None,
|
76
|
-
):
|
77
|
-
self.tasks: List[Task] = tasks
|
78
|
-
|
79
|
-
self.task_runner: BaseTaskRunner = task_runner or ConcurrentTaskRunner()
|
80
|
-
self.started: bool = False
|
81
|
-
self.stopping: bool = False
|
82
|
-
|
83
|
-
self._client = get_client()
|
84
|
-
self._exit_stack = AsyncExitStack()
|
85
|
-
|
86
|
-
if not asyncio.get_event_loop().is_running():
|
87
|
-
raise RuntimeError(
|
88
|
-
"TaskServer must be initialized within an async context."
|
89
|
-
)
|
90
|
-
|
91
|
-
self._runs_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
|
92
|
-
|
93
|
-
@property
|
94
|
-
def _client_id(self) -> str:
|
95
|
-
return f"{socket.gethostname()}-{os.getpid()}"
|
96
|
-
|
97
|
-
def handle_sigterm(self, signum, frame):
|
98
|
-
"""
|
99
|
-
Shuts down the task server when a SIGTERM is received.
|
100
|
-
"""
|
101
|
-
logger.info("SIGTERM received, initiating graceful shutdown...")
|
102
|
-
from_sync.call_in_loop_thread(create_call(self.stop))
|
103
|
-
|
104
|
-
sys.exit(0)
|
105
|
-
|
106
|
-
@sync_compatible
|
107
|
-
async def start(self) -> None:
|
108
|
-
"""
|
109
|
-
Starts a task server, which runs the tasks provided in the constructor.
|
110
|
-
"""
|
111
|
-
_register_signal(signal.SIGTERM, self.handle_sigterm)
|
112
|
-
|
113
|
-
async with asyncnullcontext() if self.started else self:
|
114
|
-
logger.info("Starting task server...")
|
115
|
-
try:
|
116
|
-
await self._subscribe_to_task_scheduling()
|
117
|
-
except InvalidStatusCode as exc:
|
118
|
-
if exc.status_code == 403:
|
119
|
-
logger.error(
|
120
|
-
"Could not establish a connection to the `/task_runs/subscriptions/scheduled`"
|
121
|
-
f" endpoint found at:\n\n {PREFECT_API_URL.value()}"
|
122
|
-
"\n\nPlease double-check the values of your"
|
123
|
-
" `PREFECT_API_URL` and `PREFECT_API_KEY` environment variables."
|
124
|
-
)
|
125
|
-
else:
|
126
|
-
raise
|
127
|
-
|
128
|
-
@sync_compatible
|
129
|
-
async def stop(self):
|
130
|
-
"""Stops the task server's polling cycle."""
|
131
|
-
if not self.started:
|
132
|
-
raise RuntimeError(
|
133
|
-
"Task server has not yet started. Please start the task server by"
|
134
|
-
" calling .start()"
|
135
|
-
)
|
136
|
-
|
137
|
-
self.started = False
|
138
|
-
self.stopping = True
|
139
|
-
|
140
|
-
raise StopTaskServer
|
141
|
-
|
142
|
-
async def _subscribe_to_task_scheduling(self):
|
143
|
-
logger.info(
|
144
|
-
f"Subscribing to tasks: {' | '.join(t.task_key.split('.')[-1] for t in self.tasks)}"
|
145
|
-
)
|
146
|
-
async for task_run in Subscription(
|
147
|
-
model=TaskRun,
|
148
|
-
path="/task_runs/subscriptions/scheduled",
|
149
|
-
keys=[task.task_key for task in self.tasks],
|
150
|
-
client_id=self._client_id,
|
151
|
-
):
|
152
|
-
logger.info(f"Received task run: {task_run.id} - {task_run.name}")
|
153
|
-
await self._submit_scheduled_task_run(task_run)
|
154
|
-
|
155
|
-
async def _submit_scheduled_task_run(self, task_run: TaskRun):
|
156
|
-
logger.debug(
|
157
|
-
f"Found task run: {task_run.name!r} in state: {task_run.state.name!r}"
|
158
|
-
)
|
159
|
-
|
160
|
-
task = next((t for t in self.tasks if t.task_key == task_run.task_key), None)
|
161
|
-
|
162
|
-
if not task:
|
163
|
-
if PREFECT_TASK_SCHEDULING_DELETE_FAILED_SUBMISSIONS.value():
|
164
|
-
logger.warning(
|
165
|
-
f"Task {task_run.name!r} not found in task server registry."
|
166
|
-
)
|
167
|
-
await self._client._client.delete(f"/task_runs/{task_run.id}")
|
168
|
-
|
169
|
-
return
|
170
|
-
|
171
|
-
# The ID of the parameters for this run are stored in the Scheduled state's
|
172
|
-
# state_details. If there is no parameters_id, then the task was created
|
173
|
-
# without parameters.
|
174
|
-
parameters = {}
|
175
|
-
if should_try_to_read_parameters(task, task_run):
|
176
|
-
parameters_id = task_run.state.state_details.task_parameters_id
|
177
|
-
task.persist_result = True
|
178
|
-
factory = await ResultFactory.from_autonomous_task(task)
|
179
|
-
try:
|
180
|
-
parameters = await factory.read_parameters(parameters_id)
|
181
|
-
except Exception as exc:
|
182
|
-
logger.exception(
|
183
|
-
f"Failed to read parameters for task run {task_run.id!r}",
|
184
|
-
exc_info=exc,
|
185
|
-
)
|
186
|
-
if PREFECT_TASK_SCHEDULING_DELETE_FAILED_SUBMISSIONS.value():
|
187
|
-
logger.info(
|
188
|
-
f"Deleting task run {task_run.id!r} because it failed to submit"
|
189
|
-
)
|
190
|
-
await self._client._client.delete(f"/task_runs/{task_run.id}")
|
191
|
-
return
|
192
|
-
|
193
|
-
logger.debug(
|
194
|
-
f"Submitting run {task_run.name!r} of task {task.name!r} to engine"
|
195
|
-
)
|
196
|
-
|
197
|
-
try:
|
198
|
-
state = await propose_state(
|
199
|
-
client=get_client(), # TODO prove that we cannot use self._client here
|
200
|
-
state=Pending(),
|
201
|
-
task_run_id=task_run.id,
|
202
|
-
)
|
203
|
-
except Abort as exc:
|
204
|
-
logger.exception(
|
205
|
-
f"Failed to submit task run {task_run.id!r} to engine", exc_info=exc
|
206
|
-
)
|
207
|
-
return
|
208
|
-
except PrefectHTTPStatusError as exc:
|
209
|
-
if exc.response.status_code == 404:
|
210
|
-
logger.warning(
|
211
|
-
f"Task run {task_run.id!r} not found. It may have been deleted."
|
212
|
-
)
|
213
|
-
return
|
214
|
-
raise
|
215
|
-
|
216
|
-
if not state.is_pending():
|
217
|
-
logger.warning(
|
218
|
-
f"Cancelling submission of task run {task_run.id!r} -"
|
219
|
-
f" server returned a non-pending state {state.type.value!r}."
|
220
|
-
)
|
221
|
-
return
|
222
|
-
|
223
|
-
emit_task_run_state_change_event(
|
224
|
-
task_run=task_run,
|
225
|
-
initial_state=task_run.state,
|
226
|
-
validated_state=state,
|
227
|
-
)
|
228
|
-
|
229
|
-
try:
|
230
|
-
self._runs_task_group.start_soon(
|
231
|
-
partial(
|
232
|
-
submit_autonomous_task_run_to_engine,
|
233
|
-
task=task,
|
234
|
-
task_run=task_run,
|
235
|
-
parameters=parameters,
|
236
|
-
task_runner=self.task_runner,
|
237
|
-
client=self._client,
|
238
|
-
)
|
239
|
-
)
|
240
|
-
except BaseException as exc:
|
241
|
-
logger.exception(
|
242
|
-
f"Failed to submit task run {task_run.id!r} to engine", exc_info=exc
|
243
|
-
)
|
244
|
-
|
245
|
-
async def execute_task_run(self, task_run: TaskRun):
|
246
|
-
"""Execute a task run in the task server."""
|
247
|
-
async with self if not self.started else asyncnullcontext():
|
248
|
-
await self._submit_scheduled_task_run(task_run)
|
249
|
-
|
250
|
-
async def __aenter__(self):
|
251
|
-
logger.debug("Starting task server...")
|
252
|
-
|
253
|
-
if self._client._closed:
|
254
|
-
self._client = get_client()
|
255
|
-
|
256
|
-
await self._exit_stack.enter_async_context(self._client)
|
257
|
-
await self._exit_stack.enter_async_context(self.task_runner.start())
|
258
|
-
await self._runs_task_group.__aenter__()
|
259
|
-
|
260
|
-
self.started = True
|
261
|
-
return self
|
262
|
-
|
263
|
-
async def __aexit__(self, *exc_info):
|
264
|
-
logger.debug("Stopping task server...")
|
265
|
-
self.started = False
|
266
|
-
await self._runs_task_group.__aexit__(*exc_info)
|
267
|
-
await self._exit_stack.__aexit__(*exc_info)
|
268
|
-
|
269
|
-
|
270
|
-
@sync_compatible
|
271
|
-
async def serve(*tasks: Task, task_runner: Optional[Type[BaseTaskRunner]] = None):
|
272
|
-
"""Serve the provided tasks so that their runs may be submitted to and executed.
|
273
|
-
in the engine. Tasks do not need to be within a flow run context to be submitted.
|
274
|
-
You must `.submit` the same task object that you pass to `serve`.
|
275
|
-
|
276
|
-
Args:
|
277
|
-
- tasks: A list of tasks to serve. When a scheduled task run is found for a
|
278
|
-
given task, the task run will be submitted to the engine for execution.
|
279
|
-
- task_runner: The task runner to use for executing the tasks. Defaults to
|
280
|
-
`ConcurrentTaskRunner`.
|
281
|
-
|
282
|
-
Example:
|
283
|
-
```python
|
284
|
-
from prefect import task
|
285
|
-
from prefect.task_server import serve
|
286
|
-
|
287
|
-
@task(log_prints=True)
|
288
|
-
def say(message: str):
|
289
|
-
print(message)
|
290
|
-
|
291
|
-
@task(log_prints=True)
|
292
|
-
def yell(message: str):
|
293
|
-
print(message.upper())
|
294
|
-
|
295
|
-
# starts a long-lived process that listens for scheduled runs of these tasks
|
296
|
-
if __name__ == "__main__":
|
297
|
-
serve(say, yell)
|
298
|
-
```
|
299
|
-
"""
|
300
|
-
if not PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING.value():
|
301
|
-
raise RuntimeError(
|
302
|
-
"To enable task scheduling, set PREFECT_EXPERIMENTAL_ENABLE_TASK_SCHEDULING"
|
303
|
-
" to True."
|
304
|
-
)
|
305
|
-
|
306
|
-
task_server = TaskServer(*tasks, task_runner=task_runner)
|
307
|
-
try:
|
308
|
-
await task_server.start()
|
309
|
-
|
310
|
-
except BaseExceptionGroup as exc: # novermin
|
311
|
-
exceptions = exc.exceptions
|
312
|
-
n_exceptions = len(exceptions)
|
313
|
-
logger.error(
|
314
|
-
f"Task worker stopped with {n_exceptions} exception{'s' if n_exceptions != 1 else ''}:"
|
315
|
-
f"\n" + "\n".join(str(e) for e in exceptions)
|
316
|
-
)
|
317
|
-
|
318
|
-
except StopTaskServer:
|
319
|
-
logger.info("Task server stopped.")
|
320
|
-
|
321
|
-
except asyncio.CancelledError:
|
322
|
-
logger.info("Task server interrupted, stopping...")
|