pyawe 0.1.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.
pyawe-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyawe
3
+ Version: 0.1.0
4
+ Summary: Python client library for the AWE (Advanced Workflow Engine) API
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/colinmanning/pyawe
7
+ Project-URL: Bug Tracker, https://github.com/colinmanning/pyawe/issues
8
+ Keywords: awe,workflow,automation,ullav
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: requests>=2.28
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+ Requires-Dist: pytest-cov>=4; extra == "dev"
23
+ Requires-Dist: responses>=0.23; extra == "dev"
24
+ Requires-Dist: ruff; extra == "dev"
25
+ Requires-Dist: mypy; extra == "dev"
26
+ Requires-Dist: types-requests; extra == "dev"
27
+
28
+ # pyawe
29
+
30
+ Python client library for the [AWE (Advanced Workflow Engine)](https://github.com/colinmanning/awe-server) API.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install pyawe
36
+ ```
37
+
38
+ ## Quick start
39
+
40
+ ```python
41
+ from pyawe import AweClient
42
+
43
+ client = AweClient(
44
+ api_url="http://localhost:8080",
45
+ auth_url="http://localhost:8081", # ullav-user-management service
46
+ )
47
+ client.login(email="user@example.com", password="secret")
48
+
49
+ # Workflows
50
+ workflows = client.workflows.list()
51
+ wf = client.workflows.create("Q1 Onboarding", is_template=False)
52
+ detail = client.workflows.get(wf.id) # WorkflowWithTasks
53
+
54
+ # Tasks
55
+ task = client.tasks.create("Review docs", workflow_id=wf.id, is_start=True)
56
+ client.tasks.update(task.id, status="In Progress")
57
+ mine = client.tasks.list_mine() # tasks assigned to me / my roles
58
+
59
+ # Jobs
60
+ job = client.jobs.create("Q1 Campaign")
61
+ client.jobs.clone_workflow(job.id, workflow_template_id)
62
+
63
+ # Notes
64
+ note = client.notes.create("task", task.id, "Looks good", is_shared=True)
65
+ client.notes.create_reply(note.id, "Agreed")
66
+
67
+ # Automated tasks
68
+ client.task_scripts.upsert(
69
+ task.id,
70
+ script_type="python",
71
+ script_body="print('hello')",
72
+ timeout_secs=60,
73
+ )
74
+ runs = client.task_runs.list(task.id)
75
+ ```
76
+
77
+ ## Example: list your tasks
78
+
79
+ Print every task assigned to the authenticated user, along with its status and
80
+ the workflow (and job, if any) it belongs to:
81
+
82
+ ```python
83
+ import os
84
+ from pyawe import AweClient
85
+
86
+ client = AweClient(
87
+ api_url=os.environ["AWE_API_URL"],
88
+ auth_url=os.environ.get("AWE_AUTH_URL", os.environ["AWE_API_URL"]),
89
+ )
90
+ client.login(email=os.environ["AWE_EMAIL"], password=os.environ["AWE_PASSWORD"])
91
+
92
+ for twc in client.tasks.list_mine():
93
+ context = twc.workflow_name
94
+ if twc.job_name:
95
+ context = f"{twc.job_name} / {twc.workflow_name}"
96
+ print(f"[{twc.task.status}] {twc.task.name} ({context})")
97
+ ```
98
+
99
+ A runnable version of this script is at `examples/list_my_tasks.py`.
100
+
101
+ ## Authentication
102
+
103
+ `AweClient` authenticates against the `ullav-user-management` service, which
104
+ issues the JWT accepted by the AWE server.
105
+
106
+ - `api_url` — AWE server base URL (e.g. `http://awe-server:8080`)
107
+ - `auth_url` — auth service base URL (e.g. `http://ullav-user-management:8081`);
108
+ omit if both services are behind the same proxy
109
+
110
+ Call `client.login(email, password)` before any other method. Tokens expire
111
+ according to the server's configuration; call `login()` again to refresh.
112
+
113
+ ## Resource clients
114
+
115
+ | Attribute | Resource |
116
+ |---|---|
117
+ | `client.workflows` | Workflow CRUD, merge, duplicate, team |
118
+ | `client.tasks` | Task CRUD, mine, decide, rework |
119
+ | `client.task_links` | Link create/delete, next/previous tasks, data bindings |
120
+ | `client.task_ports` | Port spec CRUD, input/output values |
121
+ | `client.task_scripts` | Automated task script upsert/delete |
122
+ | `client.task_runs` | Execution run history |
123
+ | `client.task_team_roles` | Team-role assignment |
124
+ | `client.jobs` | Job CRUD, clone workflow, team |
125
+ | `client.execution_profiles` | Kubernetes execution profile CRUD |
126
+ | `client.loop_blocks` | Loop block CRUD |
127
+ | `client.notes` | Note CRUD, replies, folder move |
128
+ | `client.note_folders` | Note folder CRUD |
129
+
130
+ ## Error handling
131
+
132
+ ```python
133
+ from pyawe import AweAuthError, AweNotFoundError, AweValidationError
134
+
135
+ try:
136
+ wf = client.workflows.get("non-existent-id")
137
+ except AweNotFoundError:
138
+ print("not found")
139
+ except AweAuthError:
140
+ client.login(email, password) # token expired — re-authenticate
141
+ except AweValidationError as e:
142
+ print("bad request:", e)
143
+ ```
144
+
145
+ | Exception | HTTP status |
146
+ |---|---|
147
+ | `AweAuthError` | 401 / 403, or `login()` not called |
148
+ | `AweNotFoundError` | 404 |
149
+ | `AweValidationError` | 400 |
150
+ | `AweServerError` | 5xx |
151
+ | `AweError` | base class |
152
+
153
+ ## Status values
154
+
155
+ Use the `Status` and `ScheduleStatus` enumerations or plain strings:
156
+
157
+ ```python
158
+ from pyawe import Status, ScheduleStatus
159
+
160
+ client.tasks.update(task_id, status=Status.IN_PROGRESS)
161
+ client.tasks.update(task_id, status="In Progress") # equivalent
162
+ ```
163
+
164
+ ## Licence
165
+
166
+ MIT
pyawe-0.1.0/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # pyawe
2
+
3
+ Python client library for the [AWE (Advanced Workflow Engine)](https://github.com/colinmanning/awe-server) API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pyawe
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from pyawe import AweClient
15
+
16
+ client = AweClient(
17
+ api_url="http://localhost:8080",
18
+ auth_url="http://localhost:8081", # ullav-user-management service
19
+ )
20
+ client.login(email="user@example.com", password="secret")
21
+
22
+ # Workflows
23
+ workflows = client.workflows.list()
24
+ wf = client.workflows.create("Q1 Onboarding", is_template=False)
25
+ detail = client.workflows.get(wf.id) # WorkflowWithTasks
26
+
27
+ # Tasks
28
+ task = client.tasks.create("Review docs", workflow_id=wf.id, is_start=True)
29
+ client.tasks.update(task.id, status="In Progress")
30
+ mine = client.tasks.list_mine() # tasks assigned to me / my roles
31
+
32
+ # Jobs
33
+ job = client.jobs.create("Q1 Campaign")
34
+ client.jobs.clone_workflow(job.id, workflow_template_id)
35
+
36
+ # Notes
37
+ note = client.notes.create("task", task.id, "Looks good", is_shared=True)
38
+ client.notes.create_reply(note.id, "Agreed")
39
+
40
+ # Automated tasks
41
+ client.task_scripts.upsert(
42
+ task.id,
43
+ script_type="python",
44
+ script_body="print('hello')",
45
+ timeout_secs=60,
46
+ )
47
+ runs = client.task_runs.list(task.id)
48
+ ```
49
+
50
+ ## Example: list your tasks
51
+
52
+ Print every task assigned to the authenticated user, along with its status and
53
+ the workflow (and job, if any) it belongs to:
54
+
55
+ ```python
56
+ import os
57
+ from pyawe import AweClient
58
+
59
+ client = AweClient(
60
+ api_url=os.environ["AWE_API_URL"],
61
+ auth_url=os.environ.get("AWE_AUTH_URL", os.environ["AWE_API_URL"]),
62
+ )
63
+ client.login(email=os.environ["AWE_EMAIL"], password=os.environ["AWE_PASSWORD"])
64
+
65
+ for twc in client.tasks.list_mine():
66
+ context = twc.workflow_name
67
+ if twc.job_name:
68
+ context = f"{twc.job_name} / {twc.workflow_name}"
69
+ print(f"[{twc.task.status}] {twc.task.name} ({context})")
70
+ ```
71
+
72
+ A runnable version of this script is at `examples/list_my_tasks.py`.
73
+
74
+ ## Authentication
75
+
76
+ `AweClient` authenticates against the `ullav-user-management` service, which
77
+ issues the JWT accepted by the AWE server.
78
+
79
+ - `api_url` — AWE server base URL (e.g. `http://awe-server:8080`)
80
+ - `auth_url` — auth service base URL (e.g. `http://ullav-user-management:8081`);
81
+ omit if both services are behind the same proxy
82
+
83
+ Call `client.login(email, password)` before any other method. Tokens expire
84
+ according to the server's configuration; call `login()` again to refresh.
85
+
86
+ ## Resource clients
87
+
88
+ | Attribute | Resource |
89
+ |---|---|
90
+ | `client.workflows` | Workflow CRUD, merge, duplicate, team |
91
+ | `client.tasks` | Task CRUD, mine, decide, rework |
92
+ | `client.task_links` | Link create/delete, next/previous tasks, data bindings |
93
+ | `client.task_ports` | Port spec CRUD, input/output values |
94
+ | `client.task_scripts` | Automated task script upsert/delete |
95
+ | `client.task_runs` | Execution run history |
96
+ | `client.task_team_roles` | Team-role assignment |
97
+ | `client.jobs` | Job CRUD, clone workflow, team |
98
+ | `client.execution_profiles` | Kubernetes execution profile CRUD |
99
+ | `client.loop_blocks` | Loop block CRUD |
100
+ | `client.notes` | Note CRUD, replies, folder move |
101
+ | `client.note_folders` | Note folder CRUD |
102
+
103
+ ## Error handling
104
+
105
+ ```python
106
+ from pyawe import AweAuthError, AweNotFoundError, AweValidationError
107
+
108
+ try:
109
+ wf = client.workflows.get("non-existent-id")
110
+ except AweNotFoundError:
111
+ print("not found")
112
+ except AweAuthError:
113
+ client.login(email, password) # token expired — re-authenticate
114
+ except AweValidationError as e:
115
+ print("bad request:", e)
116
+ ```
117
+
118
+ | Exception | HTTP status |
119
+ |---|---|
120
+ | `AweAuthError` | 401 / 403, or `login()` not called |
121
+ | `AweNotFoundError` | 404 |
122
+ | `AweValidationError` | 400 |
123
+ | `AweServerError` | 5xx |
124
+ | `AweError` | base class |
125
+
126
+ ## Status values
127
+
128
+ Use the `Status` and `ScheduleStatus` enumerations or plain strings:
129
+
130
+ ```python
131
+ from pyawe import Status, ScheduleStatus
132
+
133
+ client.tasks.update(task_id, status=Status.IN_PROGRESS)
134
+ client.tasks.update(task_id, status="In Progress") # equivalent
135
+ ```
136
+
137
+ ## Licence
138
+
139
+ MIT
@@ -0,0 +1,77 @@
1
+ """pyawe — Python client library for the AWE (Advanced Workflow Engine) API."""
2
+
3
+ from .client import AweClient
4
+ from .exceptions import (
5
+ AweAuthError,
6
+ AweError,
7
+ AweNotFoundError,
8
+ AweServerError,
9
+ AweValidationError,
10
+ )
11
+ from .models import (
12
+ CreateLoopBlockResponse,
13
+ DataBinding,
14
+ ExecutionProfile,
15
+ Job,
16
+ JobWithWorkflows,
17
+ LoginInfo,
18
+ LoopBlock,
19
+ LoopType,
20
+ Note,
21
+ NoteFolder,
22
+ PortDirection,
23
+ PortValueType,
24
+ ScheduleStatus,
25
+ ScriptType,
26
+ Status,
27
+ Task,
28
+ TaskLink,
29
+ TaskPortSpec,
30
+ TaskPortValues,
31
+ TaskRun,
32
+ TaskScript,
33
+ TaskTeamRole,
34
+ TaskType,
35
+ TaskWithContext,
36
+ Workflow,
37
+ WorkflowWithTasks,
38
+ )
39
+
40
+ __all__ = [
41
+ # Client
42
+ "AweClient",
43
+ # Exceptions
44
+ "AweError",
45
+ "AweAuthError",
46
+ "AweNotFoundError",
47
+ "AweValidationError",
48
+ "AweServerError",
49
+ # Enumerations
50
+ "Status",
51
+ "ScheduleStatus",
52
+ "TaskType",
53
+ "PortDirection",
54
+ "PortValueType",
55
+ "ScriptType",
56
+ "LoopType",
57
+ # Models
58
+ "LoginInfo",
59
+ "Workflow",
60
+ "WorkflowWithTasks",
61
+ "Task",
62
+ "TaskWithContext",
63
+ "TaskLink",
64
+ "TaskPortSpec",
65
+ "TaskPortValues",
66
+ "DataBinding",
67
+ "TaskScript",
68
+ "TaskRun",
69
+ "TaskTeamRole",
70
+ "Job",
71
+ "JobWithWorkflows",
72
+ "ExecutionProfile",
73
+ "LoopBlock",
74
+ "CreateLoopBlockResponse",
75
+ "Note",
76
+ "NoteFolder",
77
+ ]
@@ -0,0 +1,103 @@
1
+ """Internal HTTP session: request dispatch and error mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any, Dict, Optional
7
+
8
+ import requests
9
+
10
+ from .exceptions import AweAuthError, AweError, AweNotFoundError, AweServerError, AweValidationError
11
+
12
+
13
+ def _compact(d: Dict[str, Any]) -> Dict[str, Any]:
14
+ """Return a copy of *d* with all ``None``-valued keys removed."""
15
+ return {k: v for k, v in d.items() if v is not None}
16
+
17
+
18
+ def _str_id(value: Any) -> Optional[str]:
19
+ """Coerce a ``uuid.UUID`` or string identifier to ``str``, pass ``None`` through."""
20
+ if value is None:
21
+ return None
22
+ return str(value) if isinstance(value, uuid.UUID) else value
23
+
24
+
25
+ class _HttpSession:
26
+ """Thin wrapper around :class:`requests.Session` that handles auth headers and
27
+ maps non-2xx responses to typed exceptions."""
28
+
29
+ def __init__(self, api_url: str, auth_url: str) -> None:
30
+ self._api_url = api_url.rstrip("/")
31
+ self._auth_url = auth_url.rstrip("/")
32
+ self._session = requests.Session()
33
+ self._session.headers.update({"Content-Type": "application/json"})
34
+ self._token: Optional[str] = None
35
+
36
+ def set_token(self, token: str) -> None:
37
+ """Store the JWT and attach it to all subsequent requests."""
38
+ self._token = token
39
+ self._session.headers["Authorization"] = f"Bearer {token}"
40
+
41
+ def _require_auth(self) -> None:
42
+ if self._token is None:
43
+ raise AweAuthError("Not authenticated — call AweClient.login() first.")
44
+
45
+ def _raise_for_status(self, response: requests.Response) -> None:
46
+ if response.ok:
47
+ return
48
+ try:
49
+ body = response.json()
50
+ message = body.get("error") or body.get("message") or response.text
51
+ except Exception:
52
+ message = response.text
53
+ if response.status_code == 401:
54
+ raise AweAuthError(message)
55
+ if response.status_code == 403:
56
+ raise AweAuthError(f"Forbidden: {message}")
57
+ if response.status_code == 404:
58
+ raise AweNotFoundError(message)
59
+ if response.status_code == 400:
60
+ raise AweValidationError(message)
61
+ if response.status_code >= 500:
62
+ raise AweServerError(f"Server error {response.status_code}: {message}")
63
+ raise AweError(f"HTTP {response.status_code}: {message}")
64
+
65
+ # ── auth service ──────────────────────────────────────────────────────────
66
+
67
+ def post_auth(self, path: str, json: Any) -> Any:
68
+ """POST to the authentication service (no JWT header required)."""
69
+ response = self._session.post(f"{self._auth_url}{path}", json=json)
70
+ self._raise_for_status(response)
71
+ return response.json()
72
+
73
+ # ── AWE API ───────────────────────────────────────────────────────────────
74
+
75
+ def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
76
+ self._require_auth()
77
+ response = self._session.get(f"{self._api_url}{path}", params=params)
78
+ self._raise_for_status(response)
79
+ return response.json()
80
+
81
+ def post(self, path: str, json: Any = None) -> Any:
82
+ self._require_auth()
83
+ response = self._session.post(f"{self._api_url}{path}", json=json)
84
+ self._raise_for_status(response)
85
+ return response.json() if response.status_code != 204 else None
86
+
87
+ def put(self, path: str, json: Any = None) -> Any:
88
+ self._require_auth()
89
+ response = self._session.put(f"{self._api_url}{path}", json=json)
90
+ self._raise_for_status(response)
91
+ return response.json() if response.status_code != 204 else None
92
+
93
+ def patch(self, path: str, json: Any = None) -> Any:
94
+ self._require_auth()
95
+ response = self._session.patch(f"{self._api_url}{path}", json=json)
96
+ self._raise_for_status(response)
97
+ return response.json() if response.status_code != 204 else None
98
+
99
+ def delete(self, path: str, json: Any = None) -> Any:
100
+ self._require_auth()
101
+ response = self._session.delete(f"{self._api_url}{path}", json=json)
102
+ self._raise_for_status(response)
103
+ return response.json() if response.status_code != 204 else None