vercel-celery 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.
- vercel_celery-0.7.0/.gitignore +130 -0
- vercel_celery-0.7.0/LICENSE +21 -0
- vercel_celery-0.7.0/PKG-INFO +90 -0
- vercel_celery-0.7.0/README.md +78 -0
- vercel_celery-0.7.0/_vercel_hatch_build.py +116 -0
- vercel_celery-0.7.0/hatch_build.py +29 -0
- vercel_celery-0.7.0/pyproject.toml +64 -0
- vercel_celery-0.7.0/vercel/integrations/celery/__init__.py +141 -0
- vercel_celery-0.7.0/vercel/integrations/celery/_broker.py +1199 -0
- vercel_celery-0.7.0/vercel/integrations/celery/_result_backend.py +198 -0
- vercel_celery-0.7.0/vercel/integrations/celery/py.typed +0 -0
- vercel_celery-0.7.0/vercel/integrations/celery/version.py +3 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
|
|
5
|
+
# C extensions
|
|
6
|
+
*.so
|
|
7
|
+
|
|
8
|
+
# Distribution / packaging
|
|
9
|
+
.Python
|
|
10
|
+
build/
|
|
11
|
+
develop-eggs/
|
|
12
|
+
dist/
|
|
13
|
+
downloads/
|
|
14
|
+
eggs/
|
|
15
|
+
.eggs/
|
|
16
|
+
lib/
|
|
17
|
+
lib64/
|
|
18
|
+
parts/
|
|
19
|
+
sdist/
|
|
20
|
+
var/
|
|
21
|
+
wheels/
|
|
22
|
+
share/python-wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
MANIFEST
|
|
27
|
+
|
|
28
|
+
# PyInstallerhave
|
|
29
|
+
*.manifest
|
|
30
|
+
*.spec
|
|
31
|
+
|
|
32
|
+
# Installer logs
|
|
33
|
+
pip-log.txt
|
|
34
|
+
pip-delete-this-directory.txt
|
|
35
|
+
|
|
36
|
+
# Unit test / coverage reports
|
|
37
|
+
.benchmarks/
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
.ruff_cache/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Scrapy
|
|
57
|
+
.scrapy
|
|
58
|
+
|
|
59
|
+
# Sphinx documentation
|
|
60
|
+
docs/_build/
|
|
61
|
+
|
|
62
|
+
# PyBuilder
|
|
63
|
+
target/
|
|
64
|
+
|
|
65
|
+
# Jupyter Notebook
|
|
66
|
+
.ipynb_checkpoints
|
|
67
|
+
|
|
68
|
+
# IPython
|
|
69
|
+
profile_default/
|
|
70
|
+
ipython_config.py
|
|
71
|
+
|
|
72
|
+
# pyenv
|
|
73
|
+
.python-version
|
|
74
|
+
|
|
75
|
+
# pipenv
|
|
76
|
+
Pipfile.lock
|
|
77
|
+
|
|
78
|
+
# poetry
|
|
79
|
+
poetry.lock
|
|
80
|
+
|
|
81
|
+
# PDM
|
|
82
|
+
pdm.lock
|
|
83
|
+
.pdm.toml
|
|
84
|
+
|
|
85
|
+
# Hatch
|
|
86
|
+
.hatch/
|
|
87
|
+
|
|
88
|
+
# pyright/mypy
|
|
89
|
+
.mypy_cache/
|
|
90
|
+
.dmypy.json
|
|
91
|
+
dmypy.json
|
|
92
|
+
|
|
93
|
+
# pyre
|
|
94
|
+
.pyre/
|
|
95
|
+
|
|
96
|
+
# pytype
|
|
97
|
+
.pytype/
|
|
98
|
+
|
|
99
|
+
# Caches
|
|
100
|
+
__pypackages__/
|
|
101
|
+
|
|
102
|
+
# Editor/project files
|
|
103
|
+
.DS_Store
|
|
104
|
+
.idea/
|
|
105
|
+
.vscode/
|
|
106
|
+
.zed/
|
|
107
|
+
*.swp
|
|
108
|
+
*.swo
|
|
109
|
+
|
|
110
|
+
# Virtual environments
|
|
111
|
+
.env
|
|
112
|
+
.venv
|
|
113
|
+
.venv.*
|
|
114
|
+
env/
|
|
115
|
+
venv/
|
|
116
|
+
**/.env
|
|
117
|
+
**/.venv
|
|
118
|
+
**/.venv.*
|
|
119
|
+
**/env/
|
|
120
|
+
**/venv/
|
|
121
|
+
ENV/
|
|
122
|
+
env.bak/
|
|
123
|
+
venv.bak/
|
|
124
|
+
|
|
125
|
+
# dotenv anywhere
|
|
126
|
+
**/.env
|
|
127
|
+
**/.env.*
|
|
128
|
+
**/*.env
|
|
129
|
+
|
|
130
|
+
.claude
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Vercel Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vercel-celery
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Celery integration for Vercel Queue and Runtime Cache
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: celery<6,>=5.3
|
|
9
|
+
Requires-Dist: vercel-cache>=0.7.0
|
|
10
|
+
Requires-Dist: vercel-queue>=0.7.0
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# vercel-celery
|
|
14
|
+
|
|
15
|
+
Celery integration for Vercel Queue Service and Vercel Runtime Cache.
|
|
16
|
+
|
|
17
|
+
This package provides a Celery broker transport backed by Vercel Queue Service
|
|
18
|
+
and a Celery result backend backed by Vercel Runtime Cache.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from vercel.integrations.celery import install_vercel_celery_integration
|
|
22
|
+
|
|
23
|
+
install_vercel_celery_integration()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
By default, named Celery apps publish to app-prefixed Vercel Queue topics. For
|
|
27
|
+
example, `Celery("my_app")` maps Celery queue `emails` to the Vercel Queue topic
|
|
28
|
+
`celery-my__app-emails` after Vercel Queue name sanitization. Apps with no name
|
|
29
|
+
use unprefixed topics. To override this, set
|
|
30
|
+
`broker_transport_options = {"queue_name_prefix": "jobs-"}`. Set
|
|
31
|
+
`queue_name_prefix` to `""` to use Celery queue names as Vercel Queue topics.
|
|
32
|
+
|
|
33
|
+
The broker's Vercel Queue consumer group defaults to `celery`. Workers sharing
|
|
34
|
+
the same Vercel Queue topic and consumer group compete for tasks. Workers using
|
|
35
|
+
the same topic with different consumer groups receive fan-out copies. To
|
|
36
|
+
override it, set `broker_transport_options = {"consumer_group": "workers"}`.
|
|
37
|
+
|
|
38
|
+
The installer sets Celery's default `broker_url` to `vercel://` when Celery has
|
|
39
|
+
no broker default configured. Pass `set_default_broker=False` to opt out.
|
|
40
|
+
It also sets Celery's default `result_backend` to `vercel-runtime-cache://`
|
|
41
|
+
when Celery has no result backend configured. Pass
|
|
42
|
+
`set_default_result_backend=False` to opt out.
|
|
43
|
+
|
|
44
|
+
`vercel://` uses push delivery when `VERCEL` is truthy in the environment and
|
|
45
|
+
poll delivery otherwise. Set `broker_url = "vercel-push://"` to force push
|
|
46
|
+
delivery, or `broker_url = "vercel-poll://"` for forced polling workers, local
|
|
47
|
+
workers, and background workers.
|
|
48
|
+
|
|
49
|
+
Pass `register_queues=False` to skip automatic Vercel Queue trigger
|
|
50
|
+
registration. When using that mode, manually register each Celery app whose
|
|
51
|
+
queues should receive Vercel Queue push deliveries:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from vercel.integrations.celery import register_celery_app_queues
|
|
55
|
+
|
|
56
|
+
register_celery_app_queues(app)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
To customize Celery task result storage in Vercel Runtime Cache, configure
|
|
60
|
+
Celery's result backend transport options:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
result_backend_transport_options = {
|
|
64
|
+
"namespace": "celery-results",
|
|
65
|
+
"ttl": 3600,
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
By default, named Celery apps use an app-scoped Runtime Cache namespace. For
|
|
70
|
+
example, `Celery("my_app")` stores results under `celery-results-my_app`. Apps
|
|
71
|
+
with no name use `celery-results`. If producers and workers use different
|
|
72
|
+
Celery app names, configure a shared explicit `namespace`.
|
|
73
|
+
|
|
74
|
+
The Runtime Cache backend uses `vercel.cache`, including its normal key hashing,
|
|
75
|
+
namespacing, and local in-memory fallback when Runtime Cache is unavailable.
|
|
76
|
+
Celery result backends are not required to retain results indefinitely; results
|
|
77
|
+
may expire according to Celery's result expiration settings or the backend's own
|
|
78
|
+
retention behavior. This backend is therefore an expiring, cache-backed result
|
|
79
|
+
backend rather than durable result storage. Celery treats missing result entries
|
|
80
|
+
as pending results. By default, stored results use Celery's `result_expires`
|
|
81
|
+
value as their Runtime Cache TTL; set `result_backend_transport_options["ttl"]`
|
|
82
|
+
to override that retention period.
|
|
83
|
+
|
|
84
|
+
JSON result serialization works normally. Binary serializers, including
|
|
85
|
+
`pickle`, are stored through an internal base64 wrapper so Runtime Cache only
|
|
86
|
+
receives JSON-compatible values.
|
|
87
|
+
|
|
88
|
+
Runtime Cache is a single-key cache API, so this backend does not provide native
|
|
89
|
+
bulk result fetches or increment-backed chord counters. Result availability is
|
|
90
|
+
limited by Runtime Cache retention and eviction behavior.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# vercel-celery
|
|
2
|
+
|
|
3
|
+
Celery integration for Vercel Queue Service and Vercel Runtime Cache.
|
|
4
|
+
|
|
5
|
+
This package provides a Celery broker transport backed by Vercel Queue Service
|
|
6
|
+
and a Celery result backend backed by Vercel Runtime Cache.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from vercel.integrations.celery import install_vercel_celery_integration
|
|
10
|
+
|
|
11
|
+
install_vercel_celery_integration()
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
By default, named Celery apps publish to app-prefixed Vercel Queue topics. For
|
|
15
|
+
example, `Celery("my_app")` maps Celery queue `emails` to the Vercel Queue topic
|
|
16
|
+
`celery-my__app-emails` after Vercel Queue name sanitization. Apps with no name
|
|
17
|
+
use unprefixed topics. To override this, set
|
|
18
|
+
`broker_transport_options = {"queue_name_prefix": "jobs-"}`. Set
|
|
19
|
+
`queue_name_prefix` to `""` to use Celery queue names as Vercel Queue topics.
|
|
20
|
+
|
|
21
|
+
The broker's Vercel Queue consumer group defaults to `celery`. Workers sharing
|
|
22
|
+
the same Vercel Queue topic and consumer group compete for tasks. Workers using
|
|
23
|
+
the same topic with different consumer groups receive fan-out copies. To
|
|
24
|
+
override it, set `broker_transport_options = {"consumer_group": "workers"}`.
|
|
25
|
+
|
|
26
|
+
The installer sets Celery's default `broker_url` to `vercel://` when Celery has
|
|
27
|
+
no broker default configured. Pass `set_default_broker=False` to opt out.
|
|
28
|
+
It also sets Celery's default `result_backend` to `vercel-runtime-cache://`
|
|
29
|
+
when Celery has no result backend configured. Pass
|
|
30
|
+
`set_default_result_backend=False` to opt out.
|
|
31
|
+
|
|
32
|
+
`vercel://` uses push delivery when `VERCEL` is truthy in the environment and
|
|
33
|
+
poll delivery otherwise. Set `broker_url = "vercel-push://"` to force push
|
|
34
|
+
delivery, or `broker_url = "vercel-poll://"` for forced polling workers, local
|
|
35
|
+
workers, and background workers.
|
|
36
|
+
|
|
37
|
+
Pass `register_queues=False` to skip automatic Vercel Queue trigger
|
|
38
|
+
registration. When using that mode, manually register each Celery app whose
|
|
39
|
+
queues should receive Vercel Queue push deliveries:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from vercel.integrations.celery import register_celery_app_queues
|
|
43
|
+
|
|
44
|
+
register_celery_app_queues(app)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
To customize Celery task result storage in Vercel Runtime Cache, configure
|
|
48
|
+
Celery's result backend transport options:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
result_backend_transport_options = {
|
|
52
|
+
"namespace": "celery-results",
|
|
53
|
+
"ttl": 3600,
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
By default, named Celery apps use an app-scoped Runtime Cache namespace. For
|
|
58
|
+
example, `Celery("my_app")` stores results under `celery-results-my_app`. Apps
|
|
59
|
+
with no name use `celery-results`. If producers and workers use different
|
|
60
|
+
Celery app names, configure a shared explicit `namespace`.
|
|
61
|
+
|
|
62
|
+
The Runtime Cache backend uses `vercel.cache`, including its normal key hashing,
|
|
63
|
+
namespacing, and local in-memory fallback when Runtime Cache is unavailable.
|
|
64
|
+
Celery result backends are not required to retain results indefinitely; results
|
|
65
|
+
may expire according to Celery's result expiration settings or the backend's own
|
|
66
|
+
retention behavior. This backend is therefore an expiring, cache-backed result
|
|
67
|
+
backend rather than durable result storage. Celery treats missing result entries
|
|
68
|
+
as pending results. By default, stored results use Celery's `result_expires`
|
|
69
|
+
value as their Runtime Cache TTL; set `result_backend_transport_options["ttl"]`
|
|
70
|
+
to override that retention period.
|
|
71
|
+
|
|
72
|
+
JSON result serialization works normally. Binary serializers, including
|
|
73
|
+
`pickle`, are stored through an internal base64 wrapper so Runtime Cache only
|
|
74
|
+
receives JSON-compatible values.
|
|
75
|
+
|
|
76
|
+
Runtime Cache is a single-key cache API, so this backend does not provide native
|
|
77
|
+
bulk result fetches or increment-backed chord counters. Result availability is
|
|
78
|
+
limited by Runtime Cache retention and eviction behavior.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Hatch metadata hook for publish-time workspace dependency bounds."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from hatchling.metadata.plugin.interface import MetadataHookInterface
|
|
10
|
+
from packaging.requirements import Requirement
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import tomllib
|
|
14
|
+
except ModuleNotFoundError: # pragma: no cover - Python < 3.11
|
|
15
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WorkspaceDependenciesMetadataHook(MetadataHookInterface):
|
|
19
|
+
"""Generate package dependencies from the repo-owned dependency table."""
|
|
20
|
+
|
|
21
|
+
def update(self, metadata: dict[str, Any]) -> None:
|
|
22
|
+
"""Populate dynamic dependencies for Hatchling metadata generation."""
|
|
23
|
+
pyproject = _load_pyproject(Path(self.root))
|
|
24
|
+
release = pyproject.get("tool", {}).get("vercel", {}).get("release", {})
|
|
25
|
+
dependency_table = release.get("dependencies", {})
|
|
26
|
+
workspace_sources = pyproject.get("tool", {}).get("uv", {}).get("sources", {})
|
|
27
|
+
workspace_names = {
|
|
28
|
+
name for name, source in workspace_sources.items() if source.get("workspace") is True
|
|
29
|
+
}
|
|
30
|
+
workspace_root = _find_workspace_root(Path(self.root))
|
|
31
|
+
|
|
32
|
+
metadata["dependencies"] = [
|
|
33
|
+
_rewrite_dependency(requirement, workspace_names, workspace_root)
|
|
34
|
+
for requirement in dependency_table.get("dependencies", [])
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _load_pyproject(path: Path) -> dict[str, Any]:
|
|
39
|
+
with (path / "pyproject.toml").open("rb") as fp:
|
|
40
|
+
return tomllib.load(fp)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _find_workspace_root(start: Path) -> Path | None:
|
|
44
|
+
for path in [start, *start.parents]:
|
|
45
|
+
pyproject = path / "pyproject.toml"
|
|
46
|
+
if not pyproject.exists():
|
|
47
|
+
continue
|
|
48
|
+
data = _load_pyproject(path)
|
|
49
|
+
if "workspace" in data.get("tool", {}).get("uv", {}):
|
|
50
|
+
return path
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _rewrite_dependency(
|
|
55
|
+
requirement: str,
|
|
56
|
+
workspace_names: set[str],
|
|
57
|
+
workspace_root: Path | None,
|
|
58
|
+
) -> str:
|
|
59
|
+
parsed = Requirement(requirement)
|
|
60
|
+
normalized = parsed.name.lower().replace("_", "-")
|
|
61
|
+
if normalized not in workspace_names or workspace_root is None:
|
|
62
|
+
return requirement
|
|
63
|
+
return _with_lower_bound(parsed, _read_workspace_version(workspace_root, normalized))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _with_lower_bound(requirement: Requirement, version: str) -> str:
|
|
67
|
+
extras = f"[{','.join(sorted(requirement.extras))}]" if requirement.extras else ""
|
|
68
|
+
specifiers = [
|
|
69
|
+
str(specifier) for specifier in requirement.specifier if specifier.operator != ">="
|
|
70
|
+
]
|
|
71
|
+
specifier_text = ",".join([f">={version}", *specifiers])
|
|
72
|
+
marker = f" ; {requirement.marker}" if requirement.marker else ""
|
|
73
|
+
return f"{requirement.name}{extras}{specifier_text}{marker}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_workspace_version(workspace_root: Path, package_name: str) -> str:
|
|
77
|
+
for pattern in ("src/*/pyproject.toml", "integrations/*/pyproject.toml"):
|
|
78
|
+
for pyproject_path in workspace_root.glob(pattern):
|
|
79
|
+
version = _version_from_pyproject(pyproject_path, package_name)
|
|
80
|
+
if version is not None:
|
|
81
|
+
return version
|
|
82
|
+
raise RuntimeError(f"unknown workspace dependency {package_name!r}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _version_from_pyproject(pyproject_path: Path, package_name: str) -> str | None:
|
|
86
|
+
data = _load_pyproject(pyproject_path.parent)
|
|
87
|
+
if data.get("project", {}).get("name") != package_name:
|
|
88
|
+
return None
|
|
89
|
+
version_path = pyproject_path.parent / data["tool"]["hatch"]["version"]["path"]
|
|
90
|
+
module = ast.parse(version_path.read_text(encoding="utf-8"), filename=str(version_path))
|
|
91
|
+
for node in module.body:
|
|
92
|
+
value = _version_value(node)
|
|
93
|
+
if value is None:
|
|
94
|
+
continue
|
|
95
|
+
if isinstance(value, ast.Constant) and isinstance(value.value, str):
|
|
96
|
+
return value.value
|
|
97
|
+
raise RuntimeError(f"could not find __version__ in {version_path}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _version_value(node: ast.stmt) -> ast.expr | None:
|
|
101
|
+
if isinstance(node, ast.Assign) and any(
|
|
102
|
+
isinstance(target, ast.Name) and target.id == "__version__" for target in node.targets
|
|
103
|
+
):
|
|
104
|
+
return node.value
|
|
105
|
+
if (
|
|
106
|
+
isinstance(node, ast.AnnAssign)
|
|
107
|
+
and isinstance(node.target, ast.Name)
|
|
108
|
+
and node.target.id == "__version__"
|
|
109
|
+
):
|
|
110
|
+
return node.value
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_metadata_hook() -> type[MetadataHookInterface]:
|
|
115
|
+
"""Return the hook class used by Hatchling's custom hook loader."""
|
|
116
|
+
return WorkspaceDependenciesMetadataHook
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Load the shared Vercel Hatch metadata hook."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.util import module_from_spec, spec_from_file_location
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
from hatchling.metadata.plugin.interface import MetadataHookInterface
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_metadata_hook() -> type[MetadataHookInterface]:
|
|
13
|
+
"""Return the shared workspace dependency metadata hook."""
|
|
14
|
+
return _load_shared_hook().get_metadata_hook()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_shared_hook() -> ModuleType:
|
|
18
|
+
root = Path(__file__).resolve().parent
|
|
19
|
+
candidates = [root / "../../scripts/hatch_build.py", root / "_vercel_hatch_build.py"]
|
|
20
|
+
for candidate in candidates:
|
|
21
|
+
path = candidate.resolve()
|
|
22
|
+
if path.exists():
|
|
23
|
+
spec = spec_from_file_location("_vercel_hatch_build", path)
|
|
24
|
+
if spec is None or spec.loader is None:
|
|
25
|
+
raise RuntimeError(f"could not load Hatch hook from {path}")
|
|
26
|
+
module = module_from_spec(spec)
|
|
27
|
+
spec.loader.exec_module(module)
|
|
28
|
+
return module
|
|
29
|
+
raise RuntimeError("could not find shared Vercel Hatch metadata hook")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vercel-celery"
|
|
7
|
+
dynamic = ["version", "dependencies"]
|
|
8
|
+
description = "Celery integration for Vercel Queue and Runtime Cache"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE", "LICENSE.*"]
|
|
13
|
+
|
|
14
|
+
[tool.hatch.metadata.hooks.custom]
|
|
15
|
+
path = "hatch_build.py"
|
|
16
|
+
|
|
17
|
+
[tool.vercel.release.dependencies]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"celery>=5.3,<6",
|
|
20
|
+
"vercel-cache>=0.6.0",
|
|
21
|
+
"vercel-queue>=0.6.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.uv.sources]
|
|
25
|
+
vercel-cache = { workspace = true }
|
|
26
|
+
vercel-queue = { workspace = true }
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
extend = "../ruff.toml"
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
addopts = "--no-header --capture=tee-sys"
|
|
33
|
+
asyncio_mode = "auto"
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.version]
|
|
37
|
+
path = "vercel/integrations/celery/version.py"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.sdist]
|
|
40
|
+
force-include = { "../../scripts/hatch_build.py" = "/_vercel_hatch_build.py" }
|
|
41
|
+
include = [
|
|
42
|
+
"/vercel/integrations/celery/**/*.py",
|
|
43
|
+
"/vercel/integrations/celery/py.typed",
|
|
44
|
+
"/README.md",
|
|
45
|
+
"/pyproject.toml",
|
|
46
|
+
"/hatch_build.py",
|
|
47
|
+
"/LICENSE",
|
|
48
|
+
]
|
|
49
|
+
exclude = [
|
|
50
|
+
"/**/__pycache__",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
dev-mode-dirs = ["."]
|
|
55
|
+
only-include = [
|
|
56
|
+
"/vercel/integrations/celery",
|
|
57
|
+
]
|
|
58
|
+
exclude = [
|
|
59
|
+
"/**/__pycache__",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.poe]
|
|
63
|
+
include = "../../scripts/poe/poe.toml"
|
|
64
|
+
verbosity = -1
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Celery integration for Vercel Queue Service."""
|
|
2
|
+
|
|
3
|
+
from kombu.transport import TRANSPORT_ALIASES, virtual
|
|
4
|
+
|
|
5
|
+
from celery.app import backends as celery_backends
|
|
6
|
+
from celery.app.defaults import DEFAULTS as CELERY_DEFAULTS
|
|
7
|
+
from vercel.queue import CommunicationError, TokenResolutionError, UnauthorizedError
|
|
8
|
+
|
|
9
|
+
from ._broker import (
|
|
10
|
+
AutoChannel,
|
|
11
|
+
PollChannel,
|
|
12
|
+
PushChannel,
|
|
13
|
+
_configure_existing_app_defaults,
|
|
14
|
+
_install_app_finalize_hook,
|
|
15
|
+
_install_connection_transport_options_hook,
|
|
16
|
+
_register_existing_app_queues,
|
|
17
|
+
_set_default_broker_set_by_installer,
|
|
18
|
+
register_celery_app_queues,
|
|
19
|
+
)
|
|
20
|
+
from ._result_backend import DEFAULT_RESULT_BACKEND_ALIAS, VercelRuntimeCacheBackend
|
|
21
|
+
from .version import __version__
|
|
22
|
+
|
|
23
|
+
_DEFAULT_CONNECTION_PARAMS = {"hostname": None, "port": None}
|
|
24
|
+
# Token resolution depends on request-scoped OIDC context that may be
|
|
25
|
+
# momentarily unavailable on Celery-owned threads (and refreshes with the next
|
|
26
|
+
# push delivery). A cached token can also expire between local resolution and the
|
|
27
|
+
# remote request, producing UnauthorizedError once before the next resolution
|
|
28
|
+
# evicts it. CommunicationError is ordinary network trouble. Treat all three as
|
|
29
|
+
# recoverable connection failures so the Celery consumer restarts instead of
|
|
30
|
+
# shutting the worker down permanently. Kombu's own connection_errors use
|
|
31
|
+
# amqp.exceptions.ConnectionError, not the builtin, so CommunicationError must be
|
|
32
|
+
# listed explicitly.
|
|
33
|
+
_CONNECTION_ERRORS = (
|
|
34
|
+
*virtual.Transport.connection_errors,
|
|
35
|
+
TokenResolutionError,
|
|
36
|
+
UnauthorizedError,
|
|
37
|
+
CommunicationError,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VercelQueueTransport(virtual.Transport):
|
|
42
|
+
"""Kombu transport that auto-selects Vercel Queue Service delivery mode.
|
|
43
|
+
|
|
44
|
+
The transport uses push delivery when running on Vercel and polling otherwise. It is registered
|
|
45
|
+
as ``vercel://`` by ``install_vercel_celery_integration``.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
Channel = AutoChannel
|
|
49
|
+
driver_type = "vercel"
|
|
50
|
+
driver_name = "Vercel Queue Service"
|
|
51
|
+
default_connection_params = _DEFAULT_CONNECTION_PARAMS
|
|
52
|
+
connection_errors = _CONNECTION_ERRORS
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class VercelQueuePollTransport(virtual.Transport):
|
|
56
|
+
"""Kombu transport that always polls Vercel Queue Service.
|
|
57
|
+
|
|
58
|
+
It is registered as ``vercel-poll://`` by ``install_vercel_celery_integration`` and is intended
|
|
59
|
+
for workers that cannot receive Vercel Queue push deliveries.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
Channel = PollChannel
|
|
63
|
+
driver_type = "vercel"
|
|
64
|
+
driver_name = "Vercel Queue Service (poll)"
|
|
65
|
+
default_connection_params = _DEFAULT_CONNECTION_PARAMS
|
|
66
|
+
connection_errors = _CONNECTION_ERRORS
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class VercelQueuePushTransport(virtual.Transport):
|
|
70
|
+
"""Kombu transport that receives Vercel Queue Service push deliveries.
|
|
71
|
+
|
|
72
|
+
It is registered as ``vercel-push://`` by ``install_vercel_celery_integration`` and requires the
|
|
73
|
+
app's queues to be registered as Vercel Queue topic subscribers.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
Channel = PushChannel
|
|
77
|
+
driver_type = "vercel"
|
|
78
|
+
driver_name = "Vercel Queue Service (push)"
|
|
79
|
+
default_connection_params = _DEFAULT_CONNECTION_PARAMS
|
|
80
|
+
connection_errors = _CONNECTION_ERRORS
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def install_vercel_celery_integration(
|
|
84
|
+
*,
|
|
85
|
+
register_queues: bool = True,
|
|
86
|
+
set_default_broker: bool = True,
|
|
87
|
+
set_default_result_backend: bool = True,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Register Vercel Queue Service as a Celery broker integration.
|
|
90
|
+
|
|
91
|
+
Register ``vercel://``. ``vercel-poll://``, and ``vercel-push://`` as valid Kombu transport
|
|
92
|
+
aliases for automatic push/poll, always-poll, and always-push modes correspondingly. When
|
|
93
|
+
*set_default_broker* is ``True`` (default), also set Celery default broker URL to ``vercel://``
|
|
94
|
+
when no broker is configured already. When *set_default_result_backend* is ``True`` (default),
|
|
95
|
+
set Celery's default result backend to ``vercel-runtime-cache://`` when no result backend is
|
|
96
|
+
configured already. When *register_queues* is `True` (default), automatically register all
|
|
97
|
+
Celery queues as Vercel Queues topic subscribers.
|
|
98
|
+
|
|
99
|
+
This also registers ``vercel-runtime-cache://`` as the Vercel Runtime Cache result backend
|
|
100
|
+
alias.
|
|
101
|
+
"""
|
|
102
|
+
TRANSPORT_ALIASES["vercel"] = (
|
|
103
|
+
f"{VercelQueueTransport.__module__}:{VercelQueueTransport.__name__}"
|
|
104
|
+
)
|
|
105
|
+
TRANSPORT_ALIASES["vercel-poll"] = (
|
|
106
|
+
f"{VercelQueuePollTransport.__module__}:{VercelQueuePollTransport.__name__}"
|
|
107
|
+
)
|
|
108
|
+
TRANSPORT_ALIASES["vercel-push"] = (
|
|
109
|
+
f"{VercelQueuePushTransport.__module__}:{VercelQueuePushTransport.__name__}"
|
|
110
|
+
)
|
|
111
|
+
celery_backends.BACKEND_ALIASES[DEFAULT_RESULT_BACKEND_ALIAS] = (
|
|
112
|
+
f"{VercelRuntimeCacheBackend.__module__}:{VercelRuntimeCacheBackend.__name__}"
|
|
113
|
+
)
|
|
114
|
+
default_broker_url = "vercel://" if set_default_broker else None
|
|
115
|
+
default_result_backend = (
|
|
116
|
+
f"{DEFAULT_RESULT_BACKEND_ALIAS}://" if set_default_result_backend else None
|
|
117
|
+
)
|
|
118
|
+
if set_default_broker and CELERY_DEFAULTS.get("broker_url") is None:
|
|
119
|
+
CELERY_DEFAULTS["broker_url"] = default_broker_url
|
|
120
|
+
_set_default_broker_set_by_installer(value=True)
|
|
121
|
+
if set_default_result_backend and CELERY_DEFAULTS.get("result_backend") is None:
|
|
122
|
+
CELERY_DEFAULTS["result_backend"] = default_result_backend
|
|
123
|
+
_install_connection_transport_options_hook()
|
|
124
|
+
_install_app_finalize_hook(register_queues=register_queues)
|
|
125
|
+
_configure_existing_app_defaults(
|
|
126
|
+
broker_url=default_broker_url,
|
|
127
|
+
result_backend=default_result_backend,
|
|
128
|
+
)
|
|
129
|
+
if register_queues:
|
|
130
|
+
_register_existing_app_queues()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = [
|
|
134
|
+
"VercelQueuePollTransport",
|
|
135
|
+
"VercelQueuePushTransport",
|
|
136
|
+
"VercelQueueTransport",
|
|
137
|
+
"VercelRuntimeCacheBackend",
|
|
138
|
+
"__version__",
|
|
139
|
+
"install_vercel_celery_integration",
|
|
140
|
+
"register_celery_app_queues",
|
|
141
|
+
]
|