tay-client 0.7.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.
- tay_client-0.7.0/LICENSE +18 -0
- tay_client-0.7.0/PKG-INFO +110 -0
- tay_client-0.7.0/README.md +83 -0
- tay_client-0.7.0/pyproject.toml +42 -0
- tay_client-0.7.0/setup.cfg +4 -0
- tay_client-0.7.0/tay/__init__.py +35 -0
- tay_client-0.7.0/tay/__main__.py +67 -0
- tay_client-0.7.0/tay/client.py +1105 -0
- tay_client-0.7.0/tay/errors.py +57 -0
- tay_client-0.7.0/tay/protocol.py +232 -0
- tay_client-0.7.0/tay/py.typed +0 -0
- tay_client-0.7.0/tay/socket_path.py +98 -0
- tay_client-0.7.0/tay/task.py +269 -0
- tay_client-0.7.0/tay_client.egg-info/PKG-INFO +110 -0
- tay_client-0.7.0/tay_client.egg-info/SOURCES.txt +17 -0
- tay_client-0.7.0/tay_client.egg-info/dependency_links.txt +1 -0
- tay_client-0.7.0/tay_client.egg-info/entry_points.txt +2 -0
- tay_client-0.7.0/tay_client.egg-info/top_level.txt +1 -0
- tay_client-0.7.0/tests/test_sdk.py +233 -0
tay_client-0.7.0/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrii Sydorenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tay-client
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Python SDK for producing and executing Tay jobs
|
|
5
|
+
Author: Andrii Sydorenko
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/AndriiSydorenko1904/tay/tree/v0.7.0/clients/python
|
|
8
|
+
Project-URL: Issues, https://github.com/AndriiSydorenko1904/tay/issues
|
|
9
|
+
Project-URL: Source, https://github.com/AndriiSydorenko1904/tay
|
|
10
|
+
Keywords: background-jobs,job-queue,scheduler,unix-socket
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# Tay Python SDK
|
|
29
|
+
|
|
30
|
+
`tay` is the stdlib-only Protocol v1 producer and worker client for a local Tay
|
|
31
|
+
Unix-domain socket. `Tay()` needs no socket setting: it uses the same discovery
|
|
32
|
+
contract as the Elixir Engine (explicit path, `TAY_SOCKET_PATH`, XDG runtime,
|
|
33
|
+
`TMPDIR`, then `/tmp/tay-<uid>`). It is intentionally a separately installable
|
|
34
|
+
package:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
python -m pip install tay-client
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
From a Tay source checkout, use `python -m pip install ./clients/python`.
|
|
41
|
+
|
|
42
|
+
Create a `Tay(mode="client")` producer, or a `Tay(mode="worker")` process that
|
|
43
|
+
only executes registered tasks. The SDK sends JSON-object arguments and bounded
|
|
44
|
+
JSON results; it does not serialize arbitrary Python objects.
|
|
45
|
+
|
|
46
|
+
The producer surface is asynchronous: `enqueue`, `JobHandle.status()`,
|
|
47
|
+
`JobHandle.result()`, and `JobHandle.cancel()` map directly to the currently
|
|
48
|
+
supported Protocol v1 requests. Use a stable `submission_id` when retrying an
|
|
49
|
+
enqueue whose connection outcome is unknown.
|
|
50
|
+
|
|
51
|
+
Tasks declared before `await tay.start()` are advertised during the handshake.
|
|
52
|
+
For a long-running worker that adds or removes capabilities later, explicitly
|
|
53
|
+
sync the change:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
@tay.task(name="reports.rebuild.v1")
|
|
57
|
+
def rebuild(report_id: str) -> dict:
|
|
58
|
+
return {"report_id": report_id}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
await tay.register_tasks(rebuild)
|
|
62
|
+
# ... stop accepting new work for this task
|
|
63
|
+
await tay.unregister_tasks(rebuild)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Both changes are retained as desired capability state and reconciled on the
|
|
67
|
+
next connection. `client` mode cannot register task capabilities.
|
|
68
|
+
|
|
69
|
+
## Schedules
|
|
70
|
+
|
|
71
|
+
`schedule()` uses the five-field Unix cron form (minute, hour, day of month,
|
|
72
|
+
month, day of week). Its default timezone is `+00`; pass a fixed UTC offset
|
|
73
|
+
such as `+02`, `-05`, or `+05:30` when the schedule should be evaluated in a
|
|
74
|
+
different local clock. The API accepts `catch_up="latest"` (default) or
|
|
75
|
+
`catch_up="all"`, and defaults to `overlap="skip"`.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Every weekday at 08:00 in UTC+02.
|
|
79
|
+
await tay.schedule(
|
|
80
|
+
"reports.rebuild.v1",
|
|
81
|
+
cron="0 8 * * 1-5",
|
|
82
|
+
timezone="+02",
|
|
83
|
+
catch_up="latest",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# First run in ten minutes, then every fifteen minutes.
|
|
87
|
+
await tay.every("reports.rebuild.v1", minutes=15, delay=600)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use `start_at=<UTC milliseconds>` instead of `delay` when the first occurrence
|
|
91
|
+
has an absolute timestamp. The two options are mutually exclusive. Schedule
|
|
92
|
+
creation, execution, and cancellation work against the current listener
|
|
93
|
+
generation. Durable recovery/catch-up and enforced overlap policies are still
|
|
94
|
+
pending in the Tay engine.
|
|
95
|
+
|
|
96
|
+
Run a dedicated worker module with:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
tay-worker myapp.jobs
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The imported module must create exactly one `Tay(mode="worker")` instance and
|
|
103
|
+
register its tasks. Pass `socket_path=` or set `TAY_SOCKET_PATH` only to override
|
|
104
|
+
automatic discovery. See the repository README for the current durable-result/
|
|
105
|
+
scheduling compatibility boundary.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
The `tay-client` Python package is licensed under the MIT License. The Tay
|
|
110
|
+
engine is distributed separately under the Elastic License 2.0.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Tay Python SDK
|
|
2
|
+
|
|
3
|
+
`tay` is the stdlib-only Protocol v1 producer and worker client for a local Tay
|
|
4
|
+
Unix-domain socket. `Tay()` needs no socket setting: it uses the same discovery
|
|
5
|
+
contract as the Elixir Engine (explicit path, `TAY_SOCKET_PATH`, XDG runtime,
|
|
6
|
+
`TMPDIR`, then `/tmp/tay-<uid>`). It is intentionally a separately installable
|
|
7
|
+
package:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
python -m pip install tay-client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
From a Tay source checkout, use `python -m pip install ./clients/python`.
|
|
14
|
+
|
|
15
|
+
Create a `Tay(mode="client")` producer, or a `Tay(mode="worker")` process that
|
|
16
|
+
only executes registered tasks. The SDK sends JSON-object arguments and bounded
|
|
17
|
+
JSON results; it does not serialize arbitrary Python objects.
|
|
18
|
+
|
|
19
|
+
The producer surface is asynchronous: `enqueue`, `JobHandle.status()`,
|
|
20
|
+
`JobHandle.result()`, and `JobHandle.cancel()` map directly to the currently
|
|
21
|
+
supported Protocol v1 requests. Use a stable `submission_id` when retrying an
|
|
22
|
+
enqueue whose connection outcome is unknown.
|
|
23
|
+
|
|
24
|
+
Tasks declared before `await tay.start()` are advertised during the handshake.
|
|
25
|
+
For a long-running worker that adds or removes capabilities later, explicitly
|
|
26
|
+
sync the change:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
@tay.task(name="reports.rebuild.v1")
|
|
30
|
+
def rebuild(report_id: str) -> dict:
|
|
31
|
+
return {"report_id": report_id}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
await tay.register_tasks(rebuild)
|
|
35
|
+
# ... stop accepting new work for this task
|
|
36
|
+
await tay.unregister_tasks(rebuild)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Both changes are retained as desired capability state and reconciled on the
|
|
40
|
+
next connection. `client` mode cannot register task capabilities.
|
|
41
|
+
|
|
42
|
+
## Schedules
|
|
43
|
+
|
|
44
|
+
`schedule()` uses the five-field Unix cron form (minute, hour, day of month,
|
|
45
|
+
month, day of week). Its default timezone is `+00`; pass a fixed UTC offset
|
|
46
|
+
such as `+02`, `-05`, or `+05:30` when the schedule should be evaluated in a
|
|
47
|
+
different local clock. The API accepts `catch_up="latest"` (default) or
|
|
48
|
+
`catch_up="all"`, and defaults to `overlap="skip"`.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# Every weekday at 08:00 in UTC+02.
|
|
52
|
+
await tay.schedule(
|
|
53
|
+
"reports.rebuild.v1",
|
|
54
|
+
cron="0 8 * * 1-5",
|
|
55
|
+
timezone="+02",
|
|
56
|
+
catch_up="latest",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# First run in ten minutes, then every fifteen minutes.
|
|
60
|
+
await tay.every("reports.rebuild.v1", minutes=15, delay=600)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Use `start_at=<UTC milliseconds>` instead of `delay` when the first occurrence
|
|
64
|
+
has an absolute timestamp. The two options are mutually exclusive. Schedule
|
|
65
|
+
creation, execution, and cancellation work against the current listener
|
|
66
|
+
generation. Durable recovery/catch-up and enforced overlap policies are still
|
|
67
|
+
pending in the Tay engine.
|
|
68
|
+
|
|
69
|
+
Run a dedicated worker module with:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
tay-worker myapp.jobs
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The imported module must create exactly one `Tay(mode="worker")` instance and
|
|
76
|
+
register its tasks. Pass `socket_path=` or set `TAY_SOCKET_PATH` only to override
|
|
77
|
+
automatic discovery. See the repository README for the current durable-result/
|
|
78
|
+
scheduling compatibility boundary.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
The `tay-client` Python package is licensed under the MIT License. The Tay
|
|
83
|
+
engine is distributed separately under the Elastic License 2.0.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tay-client"
|
|
7
|
+
version = "0.7.0"
|
|
8
|
+
description = "Python SDK for producing and executing Tay jobs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{name = "Andrii Sydorenko"}]
|
|
14
|
+
keywords = ["background-jobs", "job-queue", "scheduler", "unix-socket"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: MacOS",
|
|
19
|
+
"Operating System :: POSIX",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Documentation = "https://github.com/AndriiSydorenko1904/tay/tree/v0.7.0/clients/python"
|
|
32
|
+
Issues = "https://github.com/AndriiSydorenko1904/tay/issues"
|
|
33
|
+
Source = "https://github.com/AndriiSydorenko1904/tay"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
tay-worker = "tay.__main__:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
packages = ["tay"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
tay = ["py.typed"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Public Python SDK for the Tay Protocol v1 local executor endpoint."""
|
|
2
|
+
|
|
3
|
+
from .client import JobHandle, ScheduleHandle, Tay
|
|
4
|
+
from .errors import (
|
|
5
|
+
ConnectionLost,
|
|
6
|
+
ModeError,
|
|
7
|
+
ProtocolError,
|
|
8
|
+
RemoteTaskError,
|
|
9
|
+
ResultTooLarge,
|
|
10
|
+
ServerError,
|
|
11
|
+
TaskNotRegistered,
|
|
12
|
+
TaskRegistrationError,
|
|
13
|
+
TayError,
|
|
14
|
+
ValidationError,
|
|
15
|
+
)
|
|
16
|
+
from .socket_path import resolve_socket_path
|
|
17
|
+
from .task import Task
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ConnectionLost",
|
|
21
|
+
"JobHandle",
|
|
22
|
+
"ModeError",
|
|
23
|
+
"ProtocolError",
|
|
24
|
+
"RemoteTaskError",
|
|
25
|
+
"ResultTooLarge",
|
|
26
|
+
"ScheduleHandle",
|
|
27
|
+
"ServerError",
|
|
28
|
+
"Task",
|
|
29
|
+
"TaskNotRegistered",
|
|
30
|
+
"TaskRegistrationError",
|
|
31
|
+
"Tay",
|
|
32
|
+
"TayError",
|
|
33
|
+
"ValidationError",
|
|
34
|
+
"resolve_socket_path",
|
|
35
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""The small, dependency-free dedicated-worker entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import importlib
|
|
8
|
+
import signal
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .client import Tay
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _instance(specification: str) -> Tay:
|
|
16
|
+
module_name, separator, attribute = specification.partition(":")
|
|
17
|
+
module = importlib.import_module(module_name)
|
|
18
|
+
if separator:
|
|
19
|
+
candidate: Any = getattr(module, attribute)
|
|
20
|
+
if not isinstance(candidate, Tay):
|
|
21
|
+
raise TypeError(f"{specification!r} is not a Tay instance")
|
|
22
|
+
return candidate
|
|
23
|
+
|
|
24
|
+
candidates = tuple(
|
|
25
|
+
instance for instance in Tay.instances() if instance.__module__ == "tay.client"
|
|
26
|
+
)
|
|
27
|
+
if len(candidates) != 1:
|
|
28
|
+
raise RuntimeError(
|
|
29
|
+
f"{module_name!r} must create exactly one Tay instance, or use module:attribute"
|
|
30
|
+
)
|
|
31
|
+
return candidates[0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def _serve(instance: Tay) -> None:
|
|
35
|
+
if instance.mode != "worker":
|
|
36
|
+
raise RuntimeError("tay-worker requires Tay(mode='worker')")
|
|
37
|
+
|
|
38
|
+
stopped = asyncio.Event()
|
|
39
|
+
loop = asyncio.get_running_loop()
|
|
40
|
+
for current in (signal.SIGINT, signal.SIGTERM):
|
|
41
|
+
try:
|
|
42
|
+
loop.add_signal_handler(current, stopped.set)
|
|
43
|
+
except (
|
|
44
|
+
NotImplementedError
|
|
45
|
+
): # pragma: no cover - Windows has no UDS support here
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
await instance.start()
|
|
49
|
+
try:
|
|
50
|
+
await stopped.wait()
|
|
51
|
+
finally:
|
|
52
|
+
await instance.close()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main(argv: list[str] | None = None) -> int:
|
|
56
|
+
parser = argparse.ArgumentParser(description="run a dedicated Tay Python worker")
|
|
57
|
+
parser.add_argument("application", help="module or module:Tay_instance to import")
|
|
58
|
+
arguments = parser.parse_args(argv)
|
|
59
|
+
try:
|
|
60
|
+
asyncio.run(_serve(_instance(arguments.application)))
|
|
61
|
+
except (ImportError, AttributeError, RuntimeError, TypeError) as exc:
|
|
62
|
+
parser.error(str(exc))
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__": # pragma: no cover
|
|
67
|
+
sys.exit(main())
|