workers-py 1.6.1__tar.gz → 1.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.
- {workers_py-1.6.1 → workers_py-1.7.0}/.pre-commit-config.yaml +7 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/CHANGELOG.md +17 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/PKG-INFO +3 -2
- {workers_py-1.6.1 → workers_py-1.7.0}/pyproject.toml +28 -14
- {workers_py-1.6.1 → workers_py-1.7.0}/src/pywrangler/cli.py +17 -54
- workers_py-1.7.0/src/pywrangler/sync.py +336 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/src/pywrangler/types.py +5 -4
- workers_py-1.7.0/src/pywrangler/utils.py +325 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/tests/test_cli.py +95 -122
- {workers_py-1.6.1 → workers_py-1.7.0}/tests/test_py_version_detect.py +45 -39
- {workers_py-1.6.1 → workers_py-1.7.0}/tests/test_types.py +15 -14
- {workers_py-1.6.1 → workers_py-1.7.0}/uv.lock +25 -207
- workers_py-1.6.1/src/pywrangler/sync.py +0 -433
- workers_py-1.6.1/src/pywrangler/utils.py +0 -114
- {workers_py-1.6.1 → workers_py-1.7.0}/.gitattributes +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/.github/workflows/commitlint.yml +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/.github/workflows/lint.yml +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/.github/workflows/release.yml +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/.github/workflows/tests.yml +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/.gitignore +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/CLAUDE.md +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/CONTRIBUTING.md +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/README.md +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/src/pywrangler/__init__.py +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/src/pywrangler/__main__.py +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/src/pywrangler/metadata.py +1 -1
- /workers_py-1.6.1/tests/__init__.py → /workers_py-1.7.0/src/pywrangler/py.typed +0 -0
- {workers_py-1.6.1 → workers_py-1.7.0}/workers.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.7.0 (2025-10-31)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- Better errors when unsupported packages are requested
|
|
10
|
+
([`e5000ed`](https://github.com/cloudflare/workers-py/commit/e5000eded90fb89c8f1a46dfb107f6d246f53e89))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v1.6.2 (2025-10-22)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Add workers-runtime-sdk as a dependency, update type test
|
|
18
|
+
([#41](https://github.com/cloudflare/workers-py/pull/41),
|
|
19
|
+
[`f381505`](https://github.com/cloudflare/workers-py/commit/f381505e4c9b40ddb602928d964aa3fe38936e5c))
|
|
20
|
+
|
|
21
|
+
|
|
5
22
|
## v1.6.1 (2025-10-15)
|
|
6
23
|
|
|
7
24
|
### Bug Fixes
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workers-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: A set of libraries and tools for Python Workers
|
|
5
5
|
Project-URL: Homepage, https://github.com/cloudflare/workers-py
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/cloudflare/workers-py/issues
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
11
|
Requires-Dist: click<9.0.0,>=8.0.0
|
|
12
12
|
Requires-Dist: pyjson5>=1.6.0
|
|
13
13
|
Requires-Dist: pyodide-cli
|
|
14
14
|
Requires-Dist: pyodide-py
|
|
15
15
|
Requires-Dist: rich>=13.0.0
|
|
16
|
+
Requires-Dist: workers-runtime-sdk>=0.1.0
|
|
16
17
|
Provides-Extra: build
|
|
17
18
|
Requires-Dist: uv~=0.5.23; extra == 'build'
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "workers-py"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.0"
|
|
8
8
|
description = "A set of libraries and tools for Python Workers"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Programming Language :: Python :: 3",
|
|
13
13
|
"License :: OSI Approved :: MIT License",
|
|
@@ -19,6 +19,7 @@ dependencies = [
|
|
|
19
19
|
"pyodide-cli",
|
|
20
20
|
"pyjson5>=1.6.0",
|
|
21
21
|
"pyodide-py",
|
|
22
|
+
"workers-runtime-sdk>=0.1.0",
|
|
22
23
|
]
|
|
23
24
|
|
|
24
25
|
[dependency-groups]
|
|
@@ -42,20 +43,33 @@ pywrangler = "pywrangler.cli:app"
|
|
|
42
43
|
"Bug Tracker" = "https://github.com/cloudflare/workers-py/issues"
|
|
43
44
|
|
|
44
45
|
[tool.ruff]
|
|
45
|
-
target-version = "
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
target-version = "py312"
|
|
47
|
+
lint.select = [
|
|
48
|
+
"B0", # bugbear (all B0* checks enabled by default)
|
|
49
|
+
"B904", # bugbear (Within an except clause, raise exceptions with raise ... from err)
|
|
50
|
+
"B905", # bugbear (zip() without an explicit strict= parameter set.)
|
|
51
|
+
"C4", # flake8-comprehensions
|
|
52
|
+
"C9", # mccabe complexity
|
|
53
|
+
"E", # pycodestyles
|
|
54
|
+
"F", # pyflakes
|
|
55
|
+
"I", # isort
|
|
56
|
+
"PERF", # Perflint
|
|
57
|
+
"PGH", # pygrep-hooks
|
|
58
|
+
"PL", # Pylint
|
|
59
|
+
"UP", # pyupgrade
|
|
60
|
+
"W", # pycodestyles
|
|
61
|
+
]
|
|
62
|
+
lint.ignore = ["E402", "E501", "E731", "E741", "PLW2901", "UP031"]
|
|
63
|
+
lint.flake8-comprehensions.allow-dict-calls-with-keyword-arguments = true
|
|
49
64
|
|
|
50
65
|
[tool.mypy]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
disallow_any_generics = false
|
|
66
|
+
packages = ["pywrangler"]
|
|
67
|
+
python_version = "3.12"
|
|
68
|
+
|
|
69
|
+
show_error_codes = true
|
|
70
|
+
|
|
71
|
+
strict = true
|
|
72
|
+
warn_unreachable = true
|
|
59
73
|
no_implicit_optional = true
|
|
60
74
|
|
|
61
75
|
[[tool.mypy.overrides]]
|
|
@@ -2,13 +2,17 @@ import logging
|
|
|
2
2
|
import subprocess
|
|
3
3
|
import sys
|
|
4
4
|
import textwrap
|
|
5
|
+
from typing import Never
|
|
6
|
+
|
|
5
7
|
import click
|
|
6
8
|
|
|
9
|
+
from .sync import sync
|
|
7
10
|
from .utils import (
|
|
8
|
-
setup_logging,
|
|
9
|
-
write_success,
|
|
10
11
|
WRANGLER_COMMAND,
|
|
11
12
|
WRANGLER_CREATE_COMMAND,
|
|
13
|
+
check_wrangler_version,
|
|
14
|
+
setup_logging,
|
|
15
|
+
write_success,
|
|
12
16
|
)
|
|
13
17
|
|
|
14
18
|
setup_logging()
|
|
@@ -16,7 +20,7 @@ logger = logging.getLogger("pywrangler")
|
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
class ProxyToWranglerGroup(click.Group):
|
|
19
|
-
def get_help(self, ctx):
|
|
23
|
+
def get_help(self, ctx: click.Context) -> str:
|
|
20
24
|
"""Override to add custom help content."""
|
|
21
25
|
# Get the default help text
|
|
22
26
|
help_text = super().get_help(ctx)
|
|
@@ -28,6 +32,7 @@ class ProxyToWranglerGroup(click.Group):
|
|
|
28
32
|
capture_output=True,
|
|
29
33
|
text=True,
|
|
30
34
|
timeout=10,
|
|
35
|
+
check=False,
|
|
31
36
|
)
|
|
32
37
|
if result.returncode == 0:
|
|
33
38
|
wrangler_help = result.stdout
|
|
@@ -47,7 +52,7 @@ class ProxyToWranglerGroup(click.Group):
|
|
|
47
52
|
|
|
48
53
|
return help_text
|
|
49
54
|
|
|
50
|
-
def get_command(self, ctx, cmd_name):
|
|
55
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command:
|
|
51
56
|
command = super().get_command(ctx, cmd_name)
|
|
52
57
|
|
|
53
58
|
if command is None:
|
|
@@ -58,11 +63,9 @@ class ProxyToWranglerGroup(click.Group):
|
|
|
58
63
|
remaining_args = []
|
|
59
64
|
|
|
60
65
|
if cmd_name in ["dev", "publish", "deploy", "versions"]:
|
|
61
|
-
|
|
66
|
+
sync(force=False)
|
|
62
67
|
|
|
63
68
|
if cmd_name == "dev":
|
|
64
|
-
from .sync import check_wrangler_version
|
|
65
|
-
|
|
66
69
|
check_wrangler_version()
|
|
67
70
|
|
|
68
71
|
if cmd_name == "init":
|
|
@@ -78,7 +81,7 @@ class ProxyToWranglerGroup(click.Group):
|
|
|
78
81
|
return command
|
|
79
82
|
|
|
80
83
|
|
|
81
|
-
def get_version():
|
|
84
|
+
def get_version() -> str:
|
|
82
85
|
"""Get the version of pywrangler."""
|
|
83
86
|
try:
|
|
84
87
|
from importlib.metadata import version
|
|
@@ -91,8 +94,7 @@ def get_version():
|
|
|
91
94
|
@click.group(cls=ProxyToWranglerGroup)
|
|
92
95
|
@click.option("--debug", is_flag=True, help="Enable debug logging")
|
|
93
96
|
@click.version_option(version=get_version(), prog_name="pywrangler")
|
|
94
|
-
|
|
95
|
-
def app(ctx, debug=False):
|
|
97
|
+
def app(debug: bool = False) -> None:
|
|
96
98
|
"""
|
|
97
99
|
A CLI tool for Cloudflare Workers.
|
|
98
100
|
Use 'sync' command for Python package setup.
|
|
@@ -118,7 +120,7 @@ def app(ctx, debug=False):
|
|
|
118
120
|
type=click.Path(exists=True, dir_okay=False, readable=True),
|
|
119
121
|
help="Path to Wrangler configuration file",
|
|
120
122
|
)
|
|
121
|
-
def types_command(outdir
|
|
123
|
+
def types_command(outdir: str | None, config: str | None) -> Never:
|
|
122
124
|
from .types import wrangler_types
|
|
123
125
|
|
|
124
126
|
wrangler_types(outdir, config)
|
|
@@ -127,56 +129,17 @@ def types_command(outdir=None, config=None):
|
|
|
127
129
|
|
|
128
130
|
@app.command("sync")
|
|
129
131
|
@click.option("--force", is_flag=True, help="Force sync even if no changes detected")
|
|
130
|
-
def sync_command(force=False
|
|
132
|
+
def sync_command(force: bool = False) -> None:
|
|
131
133
|
"""
|
|
132
134
|
Installs Python packages from pyproject.toml into src/vendor.
|
|
133
135
|
|
|
134
136
|
Also creates a virtual env for Workers that you can use for testing.
|
|
135
137
|
"""
|
|
136
|
-
|
|
137
|
-
from .sync import (
|
|
138
|
-
check_requirements_txt,
|
|
139
|
-
check_wrangler_config,
|
|
140
|
-
is_sync_needed,
|
|
141
|
-
create_pyodide_venv,
|
|
142
|
-
create_workers_venv,
|
|
143
|
-
parse_requirements,
|
|
144
|
-
install_requirements,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
# Check if requirements.txt does not exist.
|
|
148
|
-
check_requirements_txt()
|
|
149
|
-
|
|
150
|
-
# Check if sync is needed based on file timestamps
|
|
151
|
-
sync_needed = force or is_sync_needed()
|
|
152
|
-
if not sync_needed:
|
|
153
|
-
if directly_requested:
|
|
154
|
-
logger.warning(
|
|
155
|
-
"pyproject.toml hasn't changed since last sync, use --force to ignore timestamp check"
|
|
156
|
-
)
|
|
157
|
-
return
|
|
158
|
-
|
|
159
|
-
# Check to make sure a wrangler config file exists.
|
|
160
|
-
check_wrangler_config()
|
|
161
|
-
|
|
162
|
-
# Create .venv-workers if it doesn't exist
|
|
163
|
-
create_workers_venv()
|
|
164
|
-
|
|
165
|
-
# Set up Pyodide virtual env
|
|
166
|
-
create_pyodide_venv()
|
|
167
|
-
|
|
168
|
-
# Generate requirements.txt from pyproject.toml by directly parsing the TOML file then install into vendor folder.
|
|
169
|
-
requirements = parse_requirements()
|
|
170
|
-
if not requirements:
|
|
171
|
-
logger.warning(
|
|
172
|
-
"No dependencies found in [project.dependencies] section of pyproject.toml."
|
|
173
|
-
)
|
|
174
|
-
install_requirements(requirements)
|
|
175
|
-
|
|
138
|
+
sync(force, directly_requested=True)
|
|
176
139
|
write_success("Sync process completed successfully.")
|
|
177
140
|
|
|
178
141
|
|
|
179
|
-
def _proxy_to_wrangler(command_name, args_list):
|
|
142
|
+
def _proxy_to_wrangler(command_name: str, args_list: list[str]) -> Never:
|
|
180
143
|
command_to_run = WRANGLER_COMMAND + [command_name] + args_list
|
|
181
144
|
logger.info(f"Passing command to npx wrangler: {' '.join(command_to_run)}")
|
|
182
145
|
try:
|
|
@@ -189,7 +152,7 @@ def _proxy_to_wrangler(command_name, args_list):
|
|
|
189
152
|
click.get_current_context().exit(1)
|
|
190
153
|
|
|
191
154
|
|
|
192
|
-
def _proxy_to_create_cloudflare(args_list):
|
|
155
|
+
def _proxy_to_create_cloudflare(args_list: list[str]) -> Never:
|
|
193
156
|
command_to_run = WRANGLER_CREATE_COMMAND + args_list
|
|
194
157
|
logger.info(f"Passing command to npx create-cloudflare: {' '.join(command_to_run)}")
|
|
195
158
|
try:
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from .utils import (
|
|
12
|
+
check_uv_version,
|
|
13
|
+
check_wrangler_config,
|
|
14
|
+
find_pyproject_toml,
|
|
15
|
+
get_project_root,
|
|
16
|
+
get_pyodide_index,
|
|
17
|
+
get_python_version,
|
|
18
|
+
get_uv_pyodide_interp_name,
|
|
19
|
+
read_pyproject_toml,
|
|
20
|
+
run_command,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_venv_workers_path() -> Path:
|
|
27
|
+
return get_project_root() / ".venv-workers"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_venv_workers_token_path() -> Path:
|
|
31
|
+
return get_venv_workers_path() / ".synced"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_vendor_token_path() -> Path:
|
|
35
|
+
return get_project_root() / "python_modules/.synced"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_pyodide_venv_path() -> Path:
|
|
39
|
+
return get_venv_workers_path() / "pyodide-venv"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_requirements_txt() -> None:
|
|
43
|
+
old_requirements_txt = get_project_root() / "requirements.txt"
|
|
44
|
+
if old_requirements_txt.is_file():
|
|
45
|
+
with open(old_requirements_txt) as f:
|
|
46
|
+
requirements = f.read().splitlines()
|
|
47
|
+
logger.warning(
|
|
48
|
+
"Specifying Python Packages in requirements.txt is no longer supported, please use pyproject.toml instead.\n"
|
|
49
|
+
+ "Put the following in your pyproject.toml to vendor the packages currently in your requirements.txt:"
|
|
50
|
+
)
|
|
51
|
+
pyproject_text = "dependencies = [\n"
|
|
52
|
+
pyproject_text += ",\n".join([f' "{x}"' for x in requirements])
|
|
53
|
+
pyproject_text += "\n]"
|
|
54
|
+
logger.warning(pyproject_text)
|
|
55
|
+
|
|
56
|
+
logger.error(
|
|
57
|
+
f"{old_requirements_txt} exists. Delete the file to continue. Exiting."
|
|
58
|
+
)
|
|
59
|
+
raise click.exceptions.Exit(code=1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_venv_python_version() -> str | None:
|
|
63
|
+
"""
|
|
64
|
+
Retrieves the Python version from the virtual environment.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The Python version string or None if it cannot be determined.
|
|
68
|
+
"""
|
|
69
|
+
venv_workers_path = get_venv_workers_path()
|
|
70
|
+
venv_python = (
|
|
71
|
+
venv_workers_path / "Scripts" / "python.exe"
|
|
72
|
+
if os.name == "nt"
|
|
73
|
+
else venv_workers_path / "bin" / "python"
|
|
74
|
+
)
|
|
75
|
+
if not venv_python.is_file():
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
result = run_command(
|
|
79
|
+
[str(venv_python), "--version"], check=False, capture_output=True
|
|
80
|
+
)
|
|
81
|
+
if result.returncode != 0:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
return result.stdout.strip()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def create_workers_venv() -> None:
|
|
88
|
+
"""
|
|
89
|
+
Creates a virtual environment at `venv_workers_path` if it doesn't exist.
|
|
90
|
+
"""
|
|
91
|
+
wanted_python_version = get_python_version()
|
|
92
|
+
logger.debug(f"Using python version from wrangler config: {wanted_python_version}")
|
|
93
|
+
|
|
94
|
+
venv_workers_path = get_venv_workers_path()
|
|
95
|
+
if venv_workers_path.is_dir():
|
|
96
|
+
installed_version = _get_venv_python_version()
|
|
97
|
+
if installed_version:
|
|
98
|
+
if wanted_python_version in installed_version:
|
|
99
|
+
logger.debug(
|
|
100
|
+
f"Virtual environment at {venv_workers_path} already exists."
|
|
101
|
+
)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
logger.warning(
|
|
105
|
+
f"Recreating virtual environment at {venv_workers_path} due to Python version mismatch. "
|
|
106
|
+
f"Found {installed_version}, expected {wanted_python_version}"
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
logger.warning(
|
|
110
|
+
f"Could not determine python version for {venv_workers_path}, recreating."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
shutil.rmtree(venv_workers_path)
|
|
114
|
+
|
|
115
|
+
logger.debug(f"Creating virtual environment at {venv_workers_path}...")
|
|
116
|
+
run_command(
|
|
117
|
+
[
|
|
118
|
+
"uv",
|
|
119
|
+
"venv",
|
|
120
|
+
str(venv_workers_path),
|
|
121
|
+
"--python",
|
|
122
|
+
f"python{wanted_python_version}",
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def create_pyodide_venv() -> None:
|
|
128
|
+
pyodide_venv_path = get_pyodide_venv_path()
|
|
129
|
+
if pyodide_venv_path.is_dir():
|
|
130
|
+
logger.debug(
|
|
131
|
+
f"Pyodide virtual environment at {pyodide_venv_path} already exists."
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
check_uv_version()
|
|
136
|
+
logger.debug(f"Creating Pyodide virtual environment at {pyodide_venv_path}...")
|
|
137
|
+
pyodide_venv_path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
interp_name = get_uv_pyodide_interp_name()
|
|
139
|
+
run_command(["uv", "python", "install", interp_name])
|
|
140
|
+
run_command(["uv", "venv", pyodide_venv_path, "--python", interp_name])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def parse_requirements() -> list[str]:
|
|
144
|
+
pyproject_data = read_pyproject_toml()
|
|
145
|
+
|
|
146
|
+
# Extract dependencies from [project.dependencies]
|
|
147
|
+
dependencies = pyproject_data.get("project", {}).get("dependencies", [])
|
|
148
|
+
|
|
149
|
+
logger.info(f"Found {len(dependencies)} dependencies.")
|
|
150
|
+
return dependencies
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@contextmanager
|
|
154
|
+
def temp_requirements_file(requirements: list[str]) -> Iterator[str]:
|
|
155
|
+
# Write dependencies to a requirements.txt-style temp file.
|
|
156
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt") as temp_file:
|
|
157
|
+
temp_file.write("\n".join(requirements))
|
|
158
|
+
temp_file.flush()
|
|
159
|
+
yield temp_file.name
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _install_requirements_to_vendor(requirements: list[str]) -> None:
|
|
163
|
+
vendor_path = get_project_root() / "python_modules"
|
|
164
|
+
logger.debug(f"Using vendor path: {vendor_path}")
|
|
165
|
+
|
|
166
|
+
if len(requirements) == 0:
|
|
167
|
+
logger.warning(
|
|
168
|
+
f"Requirements list is empty. No dependencies to install in {vendor_path}."
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# Install packages into vendor directory
|
|
173
|
+
vendor_path.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
relative_vendor_path = vendor_path.relative_to(get_project_root())
|
|
175
|
+
logger.info(
|
|
176
|
+
f"Installing packages into [bold]{relative_vendor_path}[/bold]...",
|
|
177
|
+
extra={"markup": True},
|
|
178
|
+
)
|
|
179
|
+
with temp_requirements_file(requirements) as requirements_file:
|
|
180
|
+
result = run_command(
|
|
181
|
+
[
|
|
182
|
+
"uv",
|
|
183
|
+
"pip",
|
|
184
|
+
"install",
|
|
185
|
+
"--no-build",
|
|
186
|
+
"-r",
|
|
187
|
+
requirements_file,
|
|
188
|
+
"--extra-index-url",
|
|
189
|
+
get_pyodide_index(),
|
|
190
|
+
"--index-strategy",
|
|
191
|
+
"unsafe-best-match",
|
|
192
|
+
],
|
|
193
|
+
capture_output=True,
|
|
194
|
+
check=False,
|
|
195
|
+
env=os.environ | {"VIRTUAL_ENV": get_pyodide_venv_path()},
|
|
196
|
+
)
|
|
197
|
+
if result.returncode != 0:
|
|
198
|
+
logger.warning(result.stdout.strip())
|
|
199
|
+
# Handle some common failures and give nicer error messages for them.
|
|
200
|
+
lowered_stdout = result.stdout.lower()
|
|
201
|
+
if "invalid peer certificate" in lowered_stdout:
|
|
202
|
+
logger.error(
|
|
203
|
+
"Installation failed because of an invalid peer certificate. Are your systems certificates correctly installed? Do you have an Enterprise VPN enabled?"
|
|
204
|
+
)
|
|
205
|
+
elif "failed to fetch" in lowered_stdout:
|
|
206
|
+
logger.error(
|
|
207
|
+
"Installation failed because of a failed fetch. Is your network connection working?"
|
|
208
|
+
)
|
|
209
|
+
elif "no solution found when resolving dependencies" in lowered_stdout:
|
|
210
|
+
logger.error(
|
|
211
|
+
"Installation failed because the packages you requested are not supported by Python Workers. See above for details."
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
logger.error(
|
|
215
|
+
"Installation of packages into the Python Worker failed. Possibly because these packages are not currently supported. See above for details."
|
|
216
|
+
)
|
|
217
|
+
raise click.exceptions.Exit(code=result.returncode)
|
|
218
|
+
pyv = get_python_version()
|
|
219
|
+
shutil.rmtree(vendor_path)
|
|
220
|
+
shutil.copytree(
|
|
221
|
+
get_pyodide_venv_path() / f"lib/python{pyv}/site-packages", vendor_path
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Create a pyvenv.cfg file in python_modules to mark it as a virtual environment
|
|
225
|
+
(vendor_path / "pyvenv.cfg").touch()
|
|
226
|
+
get_vendor_token_path().touch()
|
|
227
|
+
|
|
228
|
+
logger.info(
|
|
229
|
+
f"Packages installed in [bold]{relative_vendor_path}[/bold].",
|
|
230
|
+
extra={"markup": True},
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _install_requirements_to_venv(requirements: list[str]) -> None:
|
|
235
|
+
# Create a requirements file for .venv-workers that includes pyodide-py
|
|
236
|
+
venv_workers_path = get_venv_workers_path()
|
|
237
|
+
project_root = get_project_root()
|
|
238
|
+
relative_venv_workers_path = venv_workers_path.relative_to(project_root)
|
|
239
|
+
requirements = requirements.copy()
|
|
240
|
+
requirements.append("pyodide-py")
|
|
241
|
+
|
|
242
|
+
logger.info(
|
|
243
|
+
f"Installing packages into [bold]{relative_venv_workers_path}[/bold]...",
|
|
244
|
+
extra={"markup": True},
|
|
245
|
+
)
|
|
246
|
+
with temp_requirements_file(requirements) as requirements_file:
|
|
247
|
+
result = run_command(
|
|
248
|
+
[
|
|
249
|
+
"uv",
|
|
250
|
+
"pip",
|
|
251
|
+
"install",
|
|
252
|
+
"-r",
|
|
253
|
+
requirements_file,
|
|
254
|
+
],
|
|
255
|
+
check=False,
|
|
256
|
+
env=os.environ | {"VIRTUAL_ENV": venv_workers_path},
|
|
257
|
+
capture_output=True,
|
|
258
|
+
)
|
|
259
|
+
if result.returncode != 0:
|
|
260
|
+
logger.warning(result.stdout.strip())
|
|
261
|
+
logger.error(
|
|
262
|
+
"Failed to install the requirements defined in your pyproject.toml file. See above for details."
|
|
263
|
+
)
|
|
264
|
+
raise click.exceptions.Exit(code=result.returncode)
|
|
265
|
+
|
|
266
|
+
get_venv_workers_token_path().touch()
|
|
267
|
+
logger.info(
|
|
268
|
+
f"Packages installed in [bold]{relative_venv_workers_path}[/bold].",
|
|
269
|
+
extra={"markup": True},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def install_requirements(requirements: list[str]) -> None:
|
|
274
|
+
# Note: the order these are executed is important.
|
|
275
|
+
# We need to install to .venv-workers first, so that we can determine if the packages requested
|
|
276
|
+
# by the user are valid.
|
|
277
|
+
_install_requirements_to_venv(requirements)
|
|
278
|
+
# Then we install the same requirements to the vendor directory. If this installation
|
|
279
|
+
# fails while the above succeeded, it implies that Pyodide does not support these package
|
|
280
|
+
# requirements which allows us to give a nicer error message to the user.
|
|
281
|
+
_install_requirements_to_vendor(requirements)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _is_out_of_date(token: Path, time: float) -> bool:
|
|
285
|
+
if not token.exists():
|
|
286
|
+
return True
|
|
287
|
+
return time > token.stat().st_mtime
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def is_sync_needed() -> bool:
|
|
291
|
+
"""
|
|
292
|
+
Checks if pyproject.toml has been modified since the last sync.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
bool: True if sync is needed, False otherwise
|
|
296
|
+
"""
|
|
297
|
+
pyproject_toml_path = find_pyproject_toml()
|
|
298
|
+
if not pyproject_toml_path.is_file():
|
|
299
|
+
# If pyproject.toml doesn't exist, we need to abort anyway
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
pyproject_mtime = pyproject_toml_path.stat().st_mtime
|
|
303
|
+
return _is_out_of_date(get_vendor_token_path(), pyproject_mtime) or _is_out_of_date(
|
|
304
|
+
get_venv_workers_token_path(), pyproject_mtime
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def sync(force: bool = False, directly_requested: bool = False) -> None:
|
|
309
|
+
# Check if requirements.txt does not exist.
|
|
310
|
+
check_requirements_txt()
|
|
311
|
+
|
|
312
|
+
# Check if sync is needed based on file timestamps
|
|
313
|
+
sync_needed = force or is_sync_needed()
|
|
314
|
+
if not sync_needed:
|
|
315
|
+
if directly_requested:
|
|
316
|
+
logger.warning(
|
|
317
|
+
"pyproject.toml hasn't changed since last sync, use --force to ignore timestamp check"
|
|
318
|
+
)
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
# Check to make sure a wrangler config file exists.
|
|
322
|
+
check_wrangler_config()
|
|
323
|
+
|
|
324
|
+
# Create .venv-workers if it doesn't exist
|
|
325
|
+
create_workers_venv()
|
|
326
|
+
|
|
327
|
+
# Set up Pyodide virtual env
|
|
328
|
+
create_pyodide_venv()
|
|
329
|
+
|
|
330
|
+
# Generate requirements.txt from pyproject.toml by directly parsing the TOML file then install into vendor folder.
|
|
331
|
+
requirements = parse_requirements()
|
|
332
|
+
if not requirements:
|
|
333
|
+
logger.warning(
|
|
334
|
+
"No dependencies found in [project.dependencies] section of pyproject.toml."
|
|
335
|
+
)
|
|
336
|
+
install_requirements(requirements)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from .utils import WRANGLER_COMMAND, run_command
|
|
2
|
-
from tempfile import TemporaryDirectory
|
|
3
|
-
from pathlib import Path
|
|
4
1
|
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from tempfile import TemporaryDirectory
|
|
4
|
+
|
|
5
|
+
from .utils import WRANGLER_COMMAND, run_command
|
|
5
6
|
|
|
6
7
|
logger = logging.getLogger(__name__)
|
|
7
8
|
|
|
@@ -26,7 +27,7 @@ PACKAGE_JSON = """
|
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def wrangler_types(outdir_arg: str | None, config: str | None, /):
|
|
30
|
+
def wrangler_types(outdir_arg: str | None, config: str | None, /) -> None:
|
|
30
31
|
args = ["types"]
|
|
31
32
|
if config:
|
|
32
33
|
args += ["--config", config]
|