prefect-aca-sessions 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.3
2
+ Name: prefect-aca-sessions
3
+ Version: 1.0.0
4
+ Summary: Prefect worker that runs flow runs in Azure Container Apps dynamic sessions
5
+ Author: Rafael Guimaraes Siqueira
6
+ Author-email: Rafael Guimaraes Siqueira <rafasiqueira@gmail.com>
7
+ Requires-Dist: azure-identity>=1.26.0
8
+ Requires-Dist: httpx>=0.28.1
9
+ Requires-Dist: prefect>=3.8.8
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+
13
+ # prefect-aca-sessions
14
+
15
+ A Prefect worker that runs flow runs in
16
+ [Azure Container Apps dynamic sessions](https://learn.microsoft.com/en-us/azure/container-apps/sessions-code-interpreter)
17
+ (code interpreter session pools). Worker type: `azure-container-apps-sessions`.
18
+
19
+ > Status: unit-tested only. It has not yet been verified against a real session pool.
20
+
21
+ ## How it works
22
+
23
+ The worker is a long-running process that polls a Prefect work pool. For each flow run it:
24
+
25
+ 1. Authenticates to the pool's management endpoint with a Microsoft Entra token
26
+ (`DefaultAzureCredential`, audience `https://dynamicsessions.io`).
27
+ 2. Starts `prefect flow-run execute` as a detached process inside a session (after
28
+ `pip install`-ing `pip_packages`). The session identifier defaults to the flow run ID.
29
+ 3. Polls the session, forwards the process output to the flow run logs, and reports the exit code.
30
+ 4. Deletes the session (unless `delete_session_on_completion` is `false`).
31
+
32
+ A single `/executions` call is limited to 220 seconds, which is why the flow runs as a
33
+ background process that is polled rather than inside one call.
34
+
35
+ ## Prerequisites
36
+
37
+ - A self-hosted Prefect server reachable from **both** the worker and the sessions.
38
+ - An Azure Container Apps **code interpreter session pool**
39
+ (`az containerapp sessionpool create --container-type PythonLTS ...`). Note its
40
+ *pool management endpoint*:
41
+ `az containerapp sessionpool show -n <pool> -g <rg> --query properties.poolManagementEndpoint -o tsv`
42
+ - An identity for the worker with the **Azure ContainerApps Session Executor** role on the pool.
43
+ - Sessions with network egress to your Prefect API (the session pool's network status must
44
+ be `EgressEnabled`) and to PyPI, or a pool with the packages you need already available.
45
+ - Python 3.12+ and [uv](https://docs.astral.sh/uv/).
46
+
47
+ ## 1. Install the worker
48
+
49
+ The package must be installed in the environment where `prefect` runs, because Prefect
50
+ discovers worker types through the `prefect.collections` entry point.
51
+
52
+ ```bash
53
+ uv pip install git+https://github.com/<you>/prefect-aca-sessions # or: uv pip install -e .
54
+ prefect worker start --help # sanity check
55
+ ```
56
+
57
+ ## 2. Point the CLI at your server
58
+
59
+ ```bash
60
+ export PREFECT_API_URL=https://prefect.example.com/api
61
+ # export PREFECT_API_KEY=... # only if your server has basic auth / a gateway key
62
+ ```
63
+
64
+ ## 3. Create the work pool
65
+
66
+ ```bash
67
+ prefect work-pool create aca-sessions --type azure-container-apps-sessions
68
+ ```
69
+
70
+ Then open the pool in the UI (Work Pools → `aca-sessions` → Edit) and set the default
71
+ **Pool management endpoint**, or leave it empty and set it per deployment.
72
+
73
+ ## 4. Authenticate the worker to Azure
74
+
75
+ `DefaultAzureCredential` is used, so any of these work:
76
+
77
+ - **Managed identity** (worker on an Azure VM, ACA, AKS): assign the Session Executor role to it.
78
+ - **Service principal**: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET`.
79
+ - **Local development**: `az login`.
80
+
81
+ ## 5. Start the worker
82
+
83
+ ```bash
84
+ prefect worker start --pool aca-sessions --type azure-container-apps-sessions
85
+ ```
86
+
87
+ Run it under a process manager (systemd, a container, etc.) for production.
88
+
89
+ ## 6. Deploy a flow to the pool
90
+
91
+ ```python
92
+ from prefect import flow
93
+
94
+
95
+ @flow(log_prints=True)
96
+ def hello():
97
+ print("running in an ACA session")
98
+
99
+
100
+ if __name__ == "__main__":
101
+ hello.from_source(
102
+ source="https://github.com/<you>/<repo>",
103
+ entrypoint="flows.py:hello",
104
+ ).deploy(
105
+ name="hello-aca",
106
+ work_pool_name="aca-sessions",
107
+ job_variables={
108
+ "pool_management_endpoint": "https://<region>.dynamicsessions.io/subscriptions/<sub>/resourceGroups/<rg>/sessionPools/<pool>",
109
+ "pip_packages": ["prefect", "pandas"],
110
+ },
111
+ )
112
+ ```
113
+
114
+ The session has to fetch the flow code itself (git source, or a pull step), because nothing
115
+ is shared with the worker's filesystem. Add private-repo credentials via the deployment's
116
+ pull steps and `env`.
117
+
118
+ ## Job variables
119
+
120
+ | Variable | Default | Description |
121
+ | --- | --- | --- |
122
+ | `pool_management_endpoint` | required | Session pool management endpoint URL |
123
+ | `api_version` | `2025-10-02-preview` | Sessions data-plane API version |
124
+ | `session_identifier` | flow run ID | Session to run in. A fixed value reuses one session across runs, so runs can see each other's files |
125
+ | `pip_packages` | `["prefect"]` | Installed in the session before the run. Pin the Prefect version to match your server |
126
+ | `poll_interval_seconds` | `10` | Seconds between status polls |
127
+ | `delete_session_on_completion` | `true` | Delete the session after the run |
128
+ | `env` | `{}` | Extra environment variables for the flow run process |
129
+ | `command` | `prefect flow-run execute` | Command run in the session |
130
+
131
+ ## Security notes
132
+
133
+ - The flow run's environment (including `PREFECT_API_URL` and any `PREFECT_API_KEY`) is sent
134
+ to the session inside the execution request. Use a dedicated, least-privilege API key.
135
+ - Session identifiers are sensitive; don't share a fixed identifier across tenants.
136
+
137
+ ## Known limitations
138
+
139
+ - Cancelling a flow run does not stop the process in the session (`kill_infrastructure` is not
140
+ implemented); delete the session to stop it.
141
+ - Session lifetime and idle limits are set on the pool and apply to long flow runs.
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ uv sync
147
+ uv run pytest
148
+ ```
@@ -0,0 +1,136 @@
1
+ # prefect-aca-sessions
2
+
3
+ A Prefect worker that runs flow runs in
4
+ [Azure Container Apps dynamic sessions](https://learn.microsoft.com/en-us/azure/container-apps/sessions-code-interpreter)
5
+ (code interpreter session pools). Worker type: `azure-container-apps-sessions`.
6
+
7
+ > Status: unit-tested only. It has not yet been verified against a real session pool.
8
+
9
+ ## How it works
10
+
11
+ The worker is a long-running process that polls a Prefect work pool. For each flow run it:
12
+
13
+ 1. Authenticates to the pool's management endpoint with a Microsoft Entra token
14
+ (`DefaultAzureCredential`, audience `https://dynamicsessions.io`).
15
+ 2. Starts `prefect flow-run execute` as a detached process inside a session (after
16
+ `pip install`-ing `pip_packages`). The session identifier defaults to the flow run ID.
17
+ 3. Polls the session, forwards the process output to the flow run logs, and reports the exit code.
18
+ 4. Deletes the session (unless `delete_session_on_completion` is `false`).
19
+
20
+ A single `/executions` call is limited to 220 seconds, which is why the flow runs as a
21
+ background process that is polled rather than inside one call.
22
+
23
+ ## Prerequisites
24
+
25
+ - A self-hosted Prefect server reachable from **both** the worker and the sessions.
26
+ - An Azure Container Apps **code interpreter session pool**
27
+ (`az containerapp sessionpool create --container-type PythonLTS ...`). Note its
28
+ *pool management endpoint*:
29
+ `az containerapp sessionpool show -n <pool> -g <rg> --query properties.poolManagementEndpoint -o tsv`
30
+ - An identity for the worker with the **Azure ContainerApps Session Executor** role on the pool.
31
+ - Sessions with network egress to your Prefect API (the session pool's network status must
32
+ be `EgressEnabled`) and to PyPI, or a pool with the packages you need already available.
33
+ - Python 3.12+ and [uv](https://docs.astral.sh/uv/).
34
+
35
+ ## 1. Install the worker
36
+
37
+ The package must be installed in the environment where `prefect` runs, because Prefect
38
+ discovers worker types through the `prefect.collections` entry point.
39
+
40
+ ```bash
41
+ uv pip install git+https://github.com/<you>/prefect-aca-sessions # or: uv pip install -e .
42
+ prefect worker start --help # sanity check
43
+ ```
44
+
45
+ ## 2. Point the CLI at your server
46
+
47
+ ```bash
48
+ export PREFECT_API_URL=https://prefect.example.com/api
49
+ # export PREFECT_API_KEY=... # only if your server has basic auth / a gateway key
50
+ ```
51
+
52
+ ## 3. Create the work pool
53
+
54
+ ```bash
55
+ prefect work-pool create aca-sessions --type azure-container-apps-sessions
56
+ ```
57
+
58
+ Then open the pool in the UI (Work Pools → `aca-sessions` → Edit) and set the default
59
+ **Pool management endpoint**, or leave it empty and set it per deployment.
60
+
61
+ ## 4. Authenticate the worker to Azure
62
+
63
+ `DefaultAzureCredential` is used, so any of these work:
64
+
65
+ - **Managed identity** (worker on an Azure VM, ACA, AKS): assign the Session Executor role to it.
66
+ - **Service principal**: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_CLIENT_SECRET`.
67
+ - **Local development**: `az login`.
68
+
69
+ ## 5. Start the worker
70
+
71
+ ```bash
72
+ prefect worker start --pool aca-sessions --type azure-container-apps-sessions
73
+ ```
74
+
75
+ Run it under a process manager (systemd, a container, etc.) for production.
76
+
77
+ ## 6. Deploy a flow to the pool
78
+
79
+ ```python
80
+ from prefect import flow
81
+
82
+
83
+ @flow(log_prints=True)
84
+ def hello():
85
+ print("running in an ACA session")
86
+
87
+
88
+ if __name__ == "__main__":
89
+ hello.from_source(
90
+ source="https://github.com/<you>/<repo>",
91
+ entrypoint="flows.py:hello",
92
+ ).deploy(
93
+ name="hello-aca",
94
+ work_pool_name="aca-sessions",
95
+ job_variables={
96
+ "pool_management_endpoint": "https://<region>.dynamicsessions.io/subscriptions/<sub>/resourceGroups/<rg>/sessionPools/<pool>",
97
+ "pip_packages": ["prefect", "pandas"],
98
+ },
99
+ )
100
+ ```
101
+
102
+ The session has to fetch the flow code itself (git source, or a pull step), because nothing
103
+ is shared with the worker's filesystem. Add private-repo credentials via the deployment's
104
+ pull steps and `env`.
105
+
106
+ ## Job variables
107
+
108
+ | Variable | Default | Description |
109
+ | --- | --- | --- |
110
+ | `pool_management_endpoint` | required | Session pool management endpoint URL |
111
+ | `api_version` | `2025-10-02-preview` | Sessions data-plane API version |
112
+ | `session_identifier` | flow run ID | Session to run in. A fixed value reuses one session across runs, so runs can see each other's files |
113
+ | `pip_packages` | `["prefect"]` | Installed in the session before the run. Pin the Prefect version to match your server |
114
+ | `poll_interval_seconds` | `10` | Seconds between status polls |
115
+ | `delete_session_on_completion` | `true` | Delete the session after the run |
116
+ | `env` | `{}` | Extra environment variables for the flow run process |
117
+ | `command` | `prefect flow-run execute` | Command run in the session |
118
+
119
+ ## Security notes
120
+
121
+ - The flow run's environment (including `PREFECT_API_URL` and any `PREFECT_API_KEY`) is sent
122
+ to the session inside the execution request. Use a dedicated, least-privilege API key.
123
+ - Session identifiers are sensitive; don't share a fixed identifier across tenants.
124
+
125
+ ## Known limitations
126
+
127
+ - Cancelling a flow run does not stop the process in the session (`kill_infrastructure` is not
128
+ implemented); delete the session to stop it.
129
+ - Session lifetime and idle limits are set on the pool and apply to long flow runs.
130
+
131
+ ## Development
132
+
133
+ ```bash
134
+ uv sync
135
+ uv run pytest
136
+ ```
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "prefect-aca-sessions"
3
+ version = "1.0.0"
4
+ description = "Prefect worker that runs flow runs in Azure Container Apps dynamic sessions"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "azure-identity>=1.26.0",
9
+ "httpx>=0.28.1",
10
+ "prefect>=3.8.8",
11
+ ]
12
+
13
+ [[project.authors]]
14
+ name = "Rafael Guimaraes Siqueira"
15
+ email = "rafasiqueira@gmail.com"
16
+
17
+ [project.entry-points."prefect.collections"]
18
+ prefect_aca_sessions = "prefect_aca_sessions"
19
+
20
+ [build-system]
21
+ requires = ["uv_build>=0.11.30,<0.12.0"]
22
+ build-backend = "uv_build"
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "pytest>=9.1.1",
27
+ "pytest-asyncio>=1.4.0",
28
+ ]
29
+
30
+ [tool.pytest.ini_options]
31
+ asyncio_mode = "auto"
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "prefect-aca-sessions"
3
+ version = "1.0.0"
4
+ description = "Prefect worker that runs flow runs in Azure Container Apps dynamic sessions"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Rafael Guimaraes Siqueira", email = "rafasiqueira@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "azure-identity>=1.26.0",
12
+ "httpx>=0.28.1",
13
+ "prefect>=3.8.8",
14
+ ]
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.11.30,<0.12.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=9.1.1",
23
+ "pytest-asyncio>=1.4.0",
24
+ ]
25
+
26
+ [project.entry-points."prefect.collections"]
27
+ prefect_aca_sessions = "prefect_aca_sessions"
28
+
29
+ [tool.pytest.ini_options]
30
+ asyncio_mode = "auto"
@@ -0,0 +1,7 @@
1
+ from prefect_aca_sessions.worker import (
2
+ ACASessionsJobConfiguration,
3
+ ACASessionsWorker,
4
+ ACASessionsWorkerResult,
5
+ )
6
+
7
+ __all__ = ["ACASessionsJobConfiguration", "ACASessionsWorker", "ACASessionsWorkerResult"]
@@ -0,0 +1,98 @@
1
+ """Minimal async client for the Azure Container Apps code interpreter management endpoint."""
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ import httpx
8
+ from azure.core.credentials_async import AsyncTokenCredential
9
+ from azure.identity.aio import DefaultAzureCredential
10
+
11
+ SESSIONS_TOKEN_SCOPE = "https://dynamicsessions.io/.default"
12
+ REQUEST_TIMEOUT_SECONDS = 300.0 # a single execution may run up to 220s
13
+
14
+
15
+ class SessionExecutionError(RuntimeError):
16
+ """The sessions endpoint rejected a request or the executed code failed."""
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ExecutionResult:
21
+ status: str
22
+ stdout: str
23
+ stderr: str
24
+
25
+
26
+ class SessionsClient:
27
+ """Runs inline Python code in a session of a code interpreter session pool."""
28
+
29
+ def __init__(
30
+ self,
31
+ pool_management_endpoint: str,
32
+ api_version: str,
33
+ credential: AsyncTokenCredential | None = None,
34
+ http_client: httpx.AsyncClient | None = None,
35
+ ) -> None:
36
+ self._endpoint = pool_management_endpoint.rstrip("/")
37
+ self._api_version = api_version
38
+ self._credential = credential or DefaultAzureCredential()
39
+ self._http = http_client or httpx.AsyncClient(timeout=REQUEST_TIMEOUT_SECONDS)
40
+
41
+ async def __aenter__(self) -> "SessionsClient":
42
+ return self
43
+
44
+ async def __aexit__(self, *exc_info: object) -> None:
45
+ await self._http.aclose()
46
+ await self._credential.close()
47
+
48
+ async def _headers(self) -> dict[str, str]:
49
+ token = await self._credential.get_token(SESSIONS_TOKEN_SCOPE)
50
+ return {"Authorization": f"Bearer {token.token}"}
51
+
52
+ def _params(self, identifier: str) -> dict[str, str]:
53
+ return {"api-version": self._api_version, "identifier": identifier}
54
+
55
+ async def execute(self, identifier: str, code: str) -> ExecutionResult:
56
+ response = await self._http.post(
57
+ f"{self._endpoint}/executions",
58
+ params=self._params(identifier),
59
+ headers=await self._headers(),
60
+ json={
61
+ "properties": {
62
+ "codeInputType": "inline",
63
+ "executionType": "synchronous",
64
+ "code": code,
65
+ }
66
+ },
67
+ )
68
+ if response.is_error:
69
+ raise SessionExecutionError(
70
+ f"Session pool returned HTTP {response.status_code}: {response.text}"
71
+ )
72
+ return _parse_execution(response.json())
73
+
74
+ async def delete_session(self, identifier: str) -> None:
75
+ response = await self._http.delete(
76
+ f"{self._endpoint}/session",
77
+ params=self._params(identifier),
78
+ headers=await self._headers(),
79
+ )
80
+ if response.is_error and response.status_code != 404:
81
+ raise SessionExecutionError(
82
+ f"Deleting session failed with HTTP {response.status_code}: {response.text}"
83
+ )
84
+
85
+
86
+ def _parse_execution(body: dict[str, Any]) -> ExecutionResult:
87
+ properties = body.get("properties", {})
88
+ result = ExecutionResult(
89
+ status=properties.get("status", "Unknown"),
90
+ stdout=properties.get("stdout", ""),
91
+ stderr=properties.get("stderr", ""),
92
+ )
93
+ if result.status != "Succeeded":
94
+ raise SessionExecutionError(
95
+ f"Code execution ended with status {result.status!r}: "
96
+ f"{json.dumps(properties.get('result'))} {result.stderr}"
97
+ )
98
+ return result
@@ -0,0 +1,55 @@
1
+ """Python snippets executed inside the session to launch and observe the flow run process."""
2
+
3
+ import json
4
+
5
+ RUN_DIR = "/mnt/data/prefect-run"
6
+
7
+
8
+ def build_start_code(command: str, env: dict[str, str], pip_packages: list[str]) -> str:
9
+ """Code that starts `command` as a detached process and returns immediately."""
10
+ spec = json.dumps({"command": command, "env": env, "pip": pip_packages})
11
+ return f"""
12
+ import json, os, subprocess
13
+ spec = json.loads({spec!r})
14
+ run_dir = {RUN_DIR!r}
15
+ os.makedirs(run_dir, exist_ok=True)
16
+ for name in ("exit_code", "output.log"):
17
+ path = os.path.join(run_dir, name)
18
+ if os.path.exists(path):
19
+ os.remove(path)
20
+ steps = []
21
+ if spec["pip"]:
22
+ steps.append("pip install --quiet " + " ".join(spec["pip"]))
23
+ steps.append(spec["command"])
24
+ script = " && ".join("(" + s + ")" for s in steps) + "; echo $? > " + run_dir + "/exit_code"
25
+ log = open(run_dir + "/output.log", "ab")
26
+ proc = subprocess.Popen(
27
+ ["sh", "-c", script],
28
+ env={{**os.environ, **spec["env"]}},
29
+ stdout=log,
30
+ stderr=subprocess.STDOUT,
31
+ start_new_session=True,
32
+ )
33
+ print(proc.pid)
34
+ """
35
+
36
+
37
+ def build_poll_code(log_offset: int) -> str:
38
+ """Code printing JSON: new log text, next log offset and the exit code (or null)."""
39
+ return f"""
40
+ import json, os
41
+ run_dir = {RUN_DIR!r}
42
+ offset = {log_offset}
43
+ exit_path = run_dir + "/exit_code"
44
+ # read the exit code first so a run finishing mid-poll never loses its last log lines
45
+ exit_code = int(open(exit_path).read().strip()) if os.path.exists(exit_path) else None
46
+ with open(run_dir + "/output.log", "rb") as f:
47
+ f.seek(offset)
48
+ chunk = f.read()
49
+ print(json.dumps({{
50
+ "log": chunk.decode("utf-8", "replace"),
51
+ "offset": offset + len(chunk),
52
+ "exit_code": exit_code,
53
+ }}))
54
+ """
55
+
@@ -0,0 +1,144 @@
1
+ import asyncio
2
+ import json
3
+
4
+ import anyio
5
+ import anyio.abc
6
+ from prefect.client.schemas.objects import FlowRun
7
+ from prefect.workers.base import (
8
+ BaseJobConfiguration,
9
+ BaseVariables,
10
+ BaseWorker,
11
+ BaseWorkerResult,
12
+ )
13
+ from pydantic import Field
14
+
15
+ from prefect_aca_sessions.client import SessionsClient
16
+ from prefect_aca_sessions.snippets import build_poll_code, build_start_code
17
+
18
+ DEFAULT_API_VERSION = "2025-10-02-preview"
19
+ DEFAULT_POLL_INTERVAL_SECONDS = 10
20
+ FAILURE_EXIT_CODE = -1
21
+
22
+
23
+ class ACASessionsJobConfiguration(BaseJobConfiguration):
24
+ """Job configuration for flow runs executed in an ACA code interpreter session."""
25
+
26
+ pool_management_endpoint: str = Field(
27
+ description=(
28
+ "Management endpoint of the code interpreter session pool, e.g. "
29
+ "https://<region>.dynamicsessions.io/subscriptions/<id>/resourceGroups/<rg>"
30
+ "/sessionPools/<pool>"
31
+ ),
32
+ json_schema_extra=dict(template="{{ pool_management_endpoint }}"),
33
+ )
34
+ api_version: str = Field(
35
+ default=DEFAULT_API_VERSION,
36
+ description="Sessions data-plane API version.",
37
+ json_schema_extra=dict(template="{{ api_version }}"),
38
+ )
39
+ session_identifier: str | None = Field(
40
+ default=None,
41
+ description="Session identifier. Defaults to the flow run ID (one session per run).",
42
+ json_schema_extra=dict(template="{{ session_identifier }}"),
43
+ )
44
+ pip_packages: list[str] = Field(
45
+ default_factory=lambda: ["prefect"],
46
+ description="Packages pip-installed in the session before the flow run starts.",
47
+ json_schema_extra=dict(template="{{ pip_packages }}"),
48
+ )
49
+ poll_interval_seconds: int = Field(
50
+ default=DEFAULT_POLL_INTERVAL_SECONDS,
51
+ ge=1,
52
+ description="Seconds between checks of the flow run process.",
53
+ json_schema_extra=dict(template="{{ poll_interval_seconds }}"),
54
+ )
55
+ delete_session_on_completion: bool = Field(
56
+ default=True,
57
+ description="Delete the session once the flow run process exits.",
58
+ json_schema_extra=dict(template="{{ delete_session_on_completion }}"),
59
+ )
60
+
61
+
62
+ class ACASessionsVariables(BaseVariables):
63
+ pool_management_endpoint: str = Field(
64
+ description="Management endpoint of the code interpreter session pool.",
65
+ )
66
+ api_version: str = Field(default=DEFAULT_API_VERSION, description="Sessions API version.")
67
+ session_identifier: str | None = Field(
68
+ default=None, description="Session identifier. Defaults to the flow run ID."
69
+ )
70
+ pip_packages: list[str] = Field(
71
+ default_factory=lambda: ["prefect"],
72
+ description="Packages pip-installed in the session before the run.",
73
+ )
74
+ poll_interval_seconds: int = Field(
75
+ default=DEFAULT_POLL_INTERVAL_SECONDS, ge=1, description="Seconds between status checks."
76
+ )
77
+ delete_session_on_completion: bool = Field(
78
+ default=True, description="Delete the session after the run."
79
+ )
80
+
81
+
82
+ class ACASessionsWorkerResult(BaseWorkerResult):
83
+ """Result of a flow run executed in an ACA session."""
84
+
85
+
86
+ class ACASessionsWorker(BaseWorker):
87
+ type = "azure-container-apps-sessions"
88
+ job_configuration = ACASessionsJobConfiguration
89
+ job_configuration_variables = ACASessionsVariables
90
+ _description = "Runs flow runs in Azure Container Apps dynamic sessions (code interpreter)."
91
+ _documentation_url = (
92
+ "https://learn.microsoft.com/en-us/azure/container-apps/sessions-code-interpreter"
93
+ )
94
+
95
+ async def run(
96
+ self,
97
+ flow_run: FlowRun,
98
+ configuration: ACASessionsJobConfiguration,
99
+ task_status: anyio.abc.TaskStatus | None = None,
100
+ ) -> ACASessionsWorkerResult:
101
+ identifier = configuration.session_identifier or str(flow_run.id)
102
+ logger = self.get_flow_run_logger(flow_run)
103
+
104
+ async with SessionsClient(
105
+ configuration.pool_management_endpoint, configuration.api_version
106
+ ) as client:
107
+ try:
108
+ exit_code = await self._execute(client, identifier, configuration, logger, task_status)
109
+ except Exception:
110
+ logger.exception("Flow run failed in session %s", identifier)
111
+ exit_code = FAILURE_EXIT_CODE
112
+ finally:
113
+ if configuration.delete_session_on_completion:
114
+ await self._delete_quietly(client, identifier, logger)
115
+
116
+ return ACASessionsWorkerResult(status_code=exit_code, identifier=identifier)
117
+
118
+ async def _execute(self, client, identifier, configuration, logger, task_status) -> int:
119
+ await client.execute(
120
+ identifier,
121
+ build_start_code(
122
+ configuration.command or "", configuration.env, configuration.pip_packages
123
+ ),
124
+ )
125
+ logger.info("Started flow run process in session %s", identifier)
126
+ if task_status is not None:
127
+ task_status.started(identifier)
128
+
129
+ offset = 0
130
+ while True:
131
+ await asyncio.sleep(configuration.poll_interval_seconds)
132
+ poll = json.loads((await client.execute(identifier, build_poll_code(offset))).stdout)
133
+ offset = poll["offset"]
134
+ if poll["log"]:
135
+ logger.info(poll["log"].rstrip())
136
+ if poll["exit_code"] is not None:
137
+ return poll["exit_code"]
138
+
139
+ @staticmethod
140
+ async def _delete_quietly(client, identifier, logger) -> None:
141
+ try:
142
+ await client.delete_session(identifier)
143
+ except Exception:
144
+ logger.warning("Could not delete session %s", identifier, exc_info=True)