workers-py 1.6.1__py3-none-any.whl → 1.7.0__py3-none-any.whl
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.
- pywrangler/cli.py +17 -54
- pywrangler/metadata.py +1 -1
- pywrangler/py.typed +0 -0
- pywrangler/sync.py +150 -247
- pywrangler/types.py +5 -4
- pywrangler/utils.py +222 -11
- {workers_py-1.6.1.dist-info → workers_py-1.7.0.dist-info}/METADATA +3 -2
- workers_py-1.7.0.dist-info/RECORD +12 -0
- workers_py-1.6.1.dist-info/RECORD +0 -11
- {workers_py-1.6.1.dist-info → workers_py-1.7.0.dist-info}/WHEEL +0 -0
- {workers_py-1.6.1.dist-info → workers_py-1.7.0.dist-info}/entry_points.txt +0 -0
pywrangler/cli.py
CHANGED
|
@@ -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:
|
pywrangler/metadata.py
CHANGED
pywrangler/py.typed
ADDED
|
File without changes
|
pywrangler/sync.py
CHANGED
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
import re
|
|
4
3
|
import shutil
|
|
5
4
|
import tempfile
|
|
5
|
+
from collections.abc import Iterator
|
|
6
6
|
from contextlib import contextmanager
|
|
7
|
-
from datetime import datetime
|
|
8
7
|
from pathlib import Path
|
|
9
|
-
from typing import Literal
|
|
10
8
|
|
|
11
9
|
import click
|
|
12
|
-
import pyjson5
|
|
13
10
|
|
|
14
11
|
from .utils import (
|
|
15
|
-
|
|
12
|
+
check_uv_version,
|
|
13
|
+
check_wrangler_config,
|
|
16
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,
|
|
17
21
|
)
|
|
18
|
-
from .metadata import PYTHON_COMPAT_VERSIONS
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
import tomllib # Standard in Python 3.11+
|
|
22
|
-
except ImportError:
|
|
23
|
-
import tomli as tomllib # For Python < 3.11
|
|
24
22
|
|
|
25
23
|
logger = logging.getLogger(__name__)
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
VENV_REQUIREMENTS_PATH = VENV_WORKERS_PATH / "temp-venv-requirements.txt"
|
|
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"
|
|
35
32
|
|
|
36
33
|
|
|
37
|
-
def
|
|
38
|
-
|
|
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"
|
|
39
44
|
if old_requirements_txt.is_file():
|
|
40
|
-
with open(old_requirements_txt
|
|
45
|
+
with open(old_requirements_txt) as f:
|
|
41
46
|
requirements = f.read().splitlines()
|
|
42
47
|
logger.warning(
|
|
43
48
|
"Specifying Python Packages in requirements.txt is no longer supported, please use pyproject.toml instead.\n"
|
|
@@ -54,118 +59,6 @@ def check_requirements_txt():
|
|
|
54
59
|
raise click.exceptions.Exit(code=1)
|
|
55
60
|
|
|
56
61
|
|
|
57
|
-
def check_wrangler_config():
|
|
58
|
-
wrangler_jsonc = PROJECT_ROOT / "wrangler.jsonc"
|
|
59
|
-
wrangler_toml = PROJECT_ROOT / "wrangler.toml"
|
|
60
|
-
if not wrangler_jsonc.is_file() and not wrangler_toml.is_file():
|
|
61
|
-
logger.error(
|
|
62
|
-
f"{wrangler_jsonc} or {wrangler_toml} not found in {PROJECT_ROOT}."
|
|
63
|
-
)
|
|
64
|
-
raise click.exceptions.Exit(code=1)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _parse_wrangler_config() -> dict:
|
|
68
|
-
"""
|
|
69
|
-
Parse wrangler configuration from either wrangler.toml or wrangler.jsonc.
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
dict: Parsed configuration data
|
|
73
|
-
"""
|
|
74
|
-
wrangler_toml = PROJECT_ROOT / "wrangler.toml"
|
|
75
|
-
wrangler_jsonc = PROJECT_ROOT / "wrangler.jsonc"
|
|
76
|
-
|
|
77
|
-
if wrangler_toml.is_file():
|
|
78
|
-
try:
|
|
79
|
-
with open(wrangler_toml, "rb") as f:
|
|
80
|
-
return tomllib.load(f)
|
|
81
|
-
except tomllib.TOMLDecodeError as e:
|
|
82
|
-
logger.error(f"Error parsing {wrangler_toml}: {e}")
|
|
83
|
-
raise click.exceptions.Exit(code=1)
|
|
84
|
-
|
|
85
|
-
if wrangler_jsonc.is_file():
|
|
86
|
-
try:
|
|
87
|
-
with open(wrangler_jsonc, "r") as f:
|
|
88
|
-
content = f.read()
|
|
89
|
-
return pyjson5.loads(content)
|
|
90
|
-
except (pyjson5.Json5DecoderError, ValueError) as e:
|
|
91
|
-
logger.error(f"Error parsing {wrangler_jsonc}: {e}")
|
|
92
|
-
raise click.exceptions.Exit(code=1)
|
|
93
|
-
|
|
94
|
-
return {}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _get_python_version() -> Literal["3.12", "3.13"]:
|
|
98
|
-
"""
|
|
99
|
-
Determine Python version from wrangler configuration.
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
Python version string
|
|
103
|
-
"""
|
|
104
|
-
config = _parse_wrangler_config()
|
|
105
|
-
|
|
106
|
-
if not config:
|
|
107
|
-
logger.error("No wrangler config found")
|
|
108
|
-
raise click.exceptions.Exit(code=1)
|
|
109
|
-
|
|
110
|
-
compat_flags = config.get("compatibility_flags", [])
|
|
111
|
-
|
|
112
|
-
if "compatibility_date" not in config:
|
|
113
|
-
logger.error("No compatibility_date specified in wrangler config")
|
|
114
|
-
raise click.exceptions.Exit(code=1)
|
|
115
|
-
try:
|
|
116
|
-
compat_date = datetime.strptime(config.get("compatibility_date"), "%Y-%m-%d")
|
|
117
|
-
except ValueError:
|
|
118
|
-
logger.error(
|
|
119
|
-
f"Invalid compatibility_date format: {config.get('compatibility_date')}"
|
|
120
|
-
)
|
|
121
|
-
raise click.exceptions.Exit(code=1)
|
|
122
|
-
|
|
123
|
-
# Check if python_workers base flag is present (required for Python workers)
|
|
124
|
-
if "python_workers" not in compat_flags:
|
|
125
|
-
logger.error("`python_workers` compat flag not specified in wrangler config")
|
|
126
|
-
raise click.exceptions.Exit(code=1)
|
|
127
|
-
|
|
128
|
-
# Find the most specific Python version based on compat flags and date
|
|
129
|
-
# Sort by version descending to prioritize newer versions
|
|
130
|
-
sorted_versions = sorted(
|
|
131
|
-
PYTHON_COMPAT_VERSIONS, key=lambda x: x.version, reverse=True
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
for py_version in sorted_versions:
|
|
135
|
-
# Check if the specific compat flag is present
|
|
136
|
-
if py_version.compat_flag in compat_flags:
|
|
137
|
-
return py_version.version
|
|
138
|
-
|
|
139
|
-
# For versions with compat_date, also check the date requirement
|
|
140
|
-
if (
|
|
141
|
-
py_version.compat_date
|
|
142
|
-
and compat_date
|
|
143
|
-
and compat_date >= py_version.compat_date
|
|
144
|
-
):
|
|
145
|
-
return py_version.version
|
|
146
|
-
|
|
147
|
-
logger.error("Could not determine Python version from wrangler config")
|
|
148
|
-
raise click.exceptions.Exit(code=1)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _get_uv_pyodide_interp_name():
|
|
152
|
-
match _get_python_version():
|
|
153
|
-
case "3.12":
|
|
154
|
-
v = "3.12.7"
|
|
155
|
-
case "3.13":
|
|
156
|
-
v = "3.13.2"
|
|
157
|
-
return f"cpython-{v}-emscripten-wasm32-musl"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _get_pyodide_index():
|
|
161
|
-
match _get_python_version():
|
|
162
|
-
case "3.12":
|
|
163
|
-
v = "0.27.7"
|
|
164
|
-
case "3.13":
|
|
165
|
-
v = "0.28.3"
|
|
166
|
-
return "https://index.pyodide.org/" + v
|
|
167
|
-
|
|
168
|
-
|
|
169
62
|
def _get_venv_python_version() -> str | None:
|
|
170
63
|
"""
|
|
171
64
|
Retrieves the Python version from the virtual environment.
|
|
@@ -173,10 +66,11 @@ def _get_venv_python_version() -> str | None:
|
|
|
173
66
|
Returns:
|
|
174
67
|
The Python version string or None if it cannot be determined.
|
|
175
68
|
"""
|
|
69
|
+
venv_workers_path = get_venv_workers_path()
|
|
176
70
|
venv_python = (
|
|
177
|
-
|
|
71
|
+
venv_workers_path / "Scripts" / "python.exe"
|
|
178
72
|
if os.name == "nt"
|
|
179
|
-
else
|
|
73
|
+
else venv_workers_path / "bin" / "python"
|
|
180
74
|
)
|
|
181
75
|
if not venv_python.is_file():
|
|
182
76
|
return None
|
|
@@ -190,136 +84,74 @@ def _get_venv_python_version() -> str | None:
|
|
|
190
84
|
return result.stdout.strip()
|
|
191
85
|
|
|
192
86
|
|
|
193
|
-
def create_workers_venv():
|
|
87
|
+
def create_workers_venv() -> None:
|
|
194
88
|
"""
|
|
195
|
-
Creates a virtual environment at `
|
|
89
|
+
Creates a virtual environment at `venv_workers_path` if it doesn't exist.
|
|
196
90
|
"""
|
|
197
|
-
wanted_python_version =
|
|
91
|
+
wanted_python_version = get_python_version()
|
|
198
92
|
logger.debug(f"Using python version from wrangler config: {wanted_python_version}")
|
|
199
93
|
|
|
200
|
-
|
|
94
|
+
venv_workers_path = get_venv_workers_path()
|
|
95
|
+
if venv_workers_path.is_dir():
|
|
201
96
|
installed_version = _get_venv_python_version()
|
|
202
97
|
if installed_version:
|
|
203
98
|
if wanted_python_version in installed_version:
|
|
204
99
|
logger.debug(
|
|
205
|
-
f"Virtual environment at {
|
|
100
|
+
f"Virtual environment at {venv_workers_path} already exists."
|
|
206
101
|
)
|
|
207
102
|
return
|
|
208
103
|
|
|
209
104
|
logger.warning(
|
|
210
|
-
f"Recreating virtual environment at {
|
|
105
|
+
f"Recreating virtual environment at {venv_workers_path} due to Python version mismatch. "
|
|
211
106
|
f"Found {installed_version}, expected {wanted_python_version}"
|
|
212
107
|
)
|
|
213
108
|
else:
|
|
214
109
|
logger.warning(
|
|
215
|
-
f"Could not determine python version for {
|
|
110
|
+
f"Could not determine python version for {venv_workers_path}, recreating."
|
|
216
111
|
)
|
|
217
112
|
|
|
218
|
-
shutil.rmtree(
|
|
113
|
+
shutil.rmtree(venv_workers_path)
|
|
219
114
|
|
|
220
|
-
logger.debug(f"Creating virtual environment at {
|
|
115
|
+
logger.debug(f"Creating virtual environment at {venv_workers_path}...")
|
|
221
116
|
run_command(
|
|
222
117
|
[
|
|
223
118
|
"uv",
|
|
224
119
|
"venv",
|
|
225
|
-
str(
|
|
120
|
+
str(venv_workers_path),
|
|
226
121
|
"--python",
|
|
227
122
|
f"python{wanted_python_version}",
|
|
228
123
|
]
|
|
229
124
|
)
|
|
230
125
|
|
|
231
126
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def check_uv_version():
|
|
237
|
-
res = run_command(["uv", "--version"], capture_output=True)
|
|
238
|
-
ver_str = res.stdout.split(" ")[1]
|
|
239
|
-
ver = tuple(int(x) for x in ver_str.split("."))
|
|
240
|
-
if ver >= MIN_UV_VERSION:
|
|
241
|
-
return
|
|
242
|
-
min_version_str = ".".join(str(x) for x in MIN_UV_VERSION)
|
|
243
|
-
logger.error(f"uv version at least {min_version_str} required, have {ver_str}.")
|
|
244
|
-
logger.error("Update uv with `uv self update`.")
|
|
245
|
-
raise click.exceptions.Exit(code=1)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def check_wrangler_version():
|
|
249
|
-
"""
|
|
250
|
-
Check that the installed wrangler version is at least 4.42.1.
|
|
251
|
-
|
|
252
|
-
Raises:
|
|
253
|
-
click.exceptions.Exit: If wrangler is not installed or version is too old.
|
|
254
|
-
"""
|
|
255
|
-
result = run_command(
|
|
256
|
-
["npx", "--yes", "wrangler", "--version"], capture_output=True, check=False
|
|
257
|
-
)
|
|
258
|
-
if result.returncode != 0:
|
|
259
|
-
logger.error("Failed to get wrangler version. Is wrangler installed?")
|
|
260
|
-
logger.error("Install wrangler with: npm install wrangler@latest")
|
|
261
|
-
raise click.exceptions.Exit(code=1)
|
|
262
|
-
|
|
263
|
-
# Parse version from output like "wrangler 4.42.1" or " ⛅️ wrangler 4.42.1"
|
|
264
|
-
version_line = result.stdout.strip()
|
|
265
|
-
# Extract version number using regex
|
|
266
|
-
version_match = re.search(r"(\d+)\.(\d+)\.(\d+)", version_line)
|
|
267
|
-
|
|
268
|
-
if not version_match:
|
|
269
|
-
logger.error(f"Could not parse wrangler version from: {version_line}")
|
|
270
|
-
logger.error("Install wrangler with: npm install wrangler@latest")
|
|
271
|
-
raise click.exceptions.Exit(code=1)
|
|
272
|
-
|
|
273
|
-
major, minor, patch = map(int, version_match.groups())
|
|
274
|
-
current_version = (major, minor, patch)
|
|
275
|
-
|
|
276
|
-
if current_version < MIN_WRANGLER_VERSION:
|
|
277
|
-
min_version_str = ".".join(str(x) for x in MIN_WRANGLER_VERSION)
|
|
278
|
-
current_version_str = ".".join(str(x) for x in current_version)
|
|
279
|
-
logger.error(
|
|
280
|
-
f"wrangler version at least {min_version_str} required, have {current_version_str}."
|
|
281
|
-
)
|
|
282
|
-
logger.error("Update wrangler with: npm install wrangler@latest")
|
|
283
|
-
raise click.exceptions.Exit(code=1)
|
|
284
|
-
|
|
285
|
-
logger.debug(
|
|
286
|
-
f"wrangler version {'.'.join(str(x) for x in current_version)} is sufficient"
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def create_pyodide_venv():
|
|
291
|
-
if PYODIDE_VENV_PATH.is_dir():
|
|
127
|
+
def create_pyodide_venv() -> None:
|
|
128
|
+
pyodide_venv_path = get_pyodide_venv_path()
|
|
129
|
+
if pyodide_venv_path.is_dir():
|
|
292
130
|
logger.debug(
|
|
293
|
-
f"Pyodide virtual environment at {
|
|
131
|
+
f"Pyodide virtual environment at {pyodide_venv_path} already exists."
|
|
294
132
|
)
|
|
295
133
|
return
|
|
296
134
|
|
|
297
135
|
check_uv_version()
|
|
298
|
-
logger.debug(f"Creating Pyodide virtual environment at {
|
|
299
|
-
|
|
300
|
-
interp_name =
|
|
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()
|
|
301
139
|
run_command(["uv", "python", "install", interp_name])
|
|
302
|
-
run_command(["uv", "venv",
|
|
140
|
+
run_command(["uv", "venv", pyodide_venv_path, "--python", interp_name])
|
|
303
141
|
|
|
304
142
|
|
|
305
143
|
def parse_requirements() -> list[str]:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
logger.info(f"Found {len(dependencies)} dependencies.")
|
|
315
|
-
return dependencies
|
|
316
|
-
except tomllib.TOMLDecodeError as e:
|
|
317
|
-
logger.error(f"Error parsing {PYPROJECT_TOML_PATH}: {str(e)}")
|
|
318
|
-
raise click.exceptions.Exit(code=1)
|
|
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
|
|
319
151
|
|
|
320
152
|
|
|
321
153
|
@contextmanager
|
|
322
|
-
def temp_requirements_file(requirements: list[str]):
|
|
154
|
+
def temp_requirements_file(requirements: list[str]) -> Iterator[str]:
|
|
323
155
|
# Write dependencies to a requirements.txt-style temp file.
|
|
324
156
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt") as temp_file:
|
|
325
157
|
temp_file.write("\n".join(requirements))
|
|
@@ -327,8 +159,8 @@ def temp_requirements_file(requirements: list[str]):
|
|
|
327
159
|
yield temp_file.name
|
|
328
160
|
|
|
329
161
|
|
|
330
|
-
def _install_requirements_to_vendor(requirements: list[str]):
|
|
331
|
-
vendor_path =
|
|
162
|
+
def _install_requirements_to_vendor(requirements: list[str]) -> None:
|
|
163
|
+
vendor_path = get_project_root() / "python_modules"
|
|
332
164
|
logger.debug(f"Using vendor path: {vendor_path}")
|
|
333
165
|
|
|
334
166
|
if len(requirements) == 0:
|
|
@@ -339,13 +171,13 @@ def _install_requirements_to_vendor(requirements: list[str]):
|
|
|
339
171
|
|
|
340
172
|
# Install packages into vendor directory
|
|
341
173
|
vendor_path.mkdir(parents=True, exist_ok=True)
|
|
342
|
-
relative_vendor_path = vendor_path.relative_to(
|
|
174
|
+
relative_vendor_path = vendor_path.relative_to(get_project_root())
|
|
343
175
|
logger.info(
|
|
344
176
|
f"Installing packages into [bold]{relative_vendor_path}[/bold]...",
|
|
345
177
|
extra={"markup": True},
|
|
346
178
|
)
|
|
347
179
|
with temp_requirements_file(requirements) as requirements_file:
|
|
348
|
-
run_command(
|
|
180
|
+
result = run_command(
|
|
349
181
|
[
|
|
350
182
|
"uv",
|
|
351
183
|
"pip",
|
|
@@ -354,21 +186,44 @@ def _install_requirements_to_vendor(requirements: list[str]):
|
|
|
354
186
|
"-r",
|
|
355
187
|
requirements_file,
|
|
356
188
|
"--extra-index-url",
|
|
357
|
-
|
|
189
|
+
get_pyodide_index(),
|
|
358
190
|
"--index-strategy",
|
|
359
191
|
"unsafe-best-match",
|
|
360
192
|
],
|
|
361
|
-
|
|
193
|
+
capture_output=True,
|
|
194
|
+
check=False,
|
|
195
|
+
env=os.environ | {"VIRTUAL_ENV": get_pyodide_venv_path()},
|
|
362
196
|
)
|
|
363
|
-
|
|
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()
|
|
364
219
|
shutil.rmtree(vendor_path)
|
|
365
220
|
shutil.copytree(
|
|
366
|
-
|
|
221
|
+
get_pyodide_venv_path() / f"lib/python{pyv}/site-packages", vendor_path
|
|
367
222
|
)
|
|
368
223
|
|
|
369
224
|
# Create a pyvenv.cfg file in python_modules to mark it as a virtual environment
|
|
370
225
|
(vendor_path / "pyvenv.cfg").touch()
|
|
371
|
-
|
|
226
|
+
get_vendor_token_path().touch()
|
|
372
227
|
|
|
373
228
|
logger.info(
|
|
374
229
|
f"Packages installed in [bold]{relative_vendor_path}[/bold].",
|
|
@@ -376,9 +231,11 @@ def _install_requirements_to_vendor(requirements: list[str]):
|
|
|
376
231
|
)
|
|
377
232
|
|
|
378
233
|
|
|
379
|
-
def _install_requirements_to_venv(requirements: list[str]):
|
|
234
|
+
def _install_requirements_to_venv(requirements: list[str]) -> None:
|
|
380
235
|
# Create a requirements file for .venv-workers that includes pyodide-py
|
|
381
|
-
|
|
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)
|
|
382
239
|
requirements = requirements.copy()
|
|
383
240
|
requirements.append("pyodide-py")
|
|
384
241
|
|
|
@@ -387,7 +244,7 @@ def _install_requirements_to_venv(requirements: list[str]):
|
|
|
387
244
|
extra={"markup": True},
|
|
388
245
|
)
|
|
389
246
|
with temp_requirements_file(requirements) as requirements_file:
|
|
390
|
-
run_command(
|
|
247
|
+
result = run_command(
|
|
391
248
|
[
|
|
392
249
|
"uv",
|
|
393
250
|
"pip",
|
|
@@ -395,18 +252,33 @@ def _install_requirements_to_venv(requirements: list[str]):
|
|
|
395
252
|
"-r",
|
|
396
253
|
requirements_file,
|
|
397
254
|
],
|
|
398
|
-
|
|
255
|
+
check=False,
|
|
256
|
+
env=os.environ | {"VIRTUAL_ENV": venv_workers_path},
|
|
257
|
+
capture_output=True,
|
|
399
258
|
)
|
|
400
|
-
|
|
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()
|
|
401
267
|
logger.info(
|
|
402
268
|
f"Packages installed in [bold]{relative_venv_workers_path}[/bold].",
|
|
403
269
|
extra={"markup": True},
|
|
404
270
|
)
|
|
405
271
|
|
|
406
272
|
|
|
407
|
-
def install_requirements(requirements: list[str]):
|
|
408
|
-
|
|
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.
|
|
409
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)
|
|
410
282
|
|
|
411
283
|
|
|
412
284
|
def _is_out_of_date(token: Path, time: float) -> bool:
|
|
@@ -415,19 +287,50 @@ def _is_out_of_date(token: Path, time: float) -> bool:
|
|
|
415
287
|
return time > token.stat().st_mtime
|
|
416
288
|
|
|
417
289
|
|
|
418
|
-
def is_sync_needed():
|
|
290
|
+
def is_sync_needed() -> bool:
|
|
419
291
|
"""
|
|
420
292
|
Checks if pyproject.toml has been modified since the last sync.
|
|
421
293
|
|
|
422
294
|
Returns:
|
|
423
295
|
bool: True if sync is needed, False otherwise
|
|
424
296
|
"""
|
|
425
|
-
|
|
426
|
-
if not
|
|
297
|
+
pyproject_toml_path = find_pyproject_toml()
|
|
298
|
+
if not pyproject_toml_path.is_file():
|
|
427
299
|
# If pyproject.toml doesn't exist, we need to abort anyway
|
|
428
300
|
return True
|
|
429
301
|
|
|
430
|
-
pyproject_mtime =
|
|
431
|
-
return _is_out_of_date(
|
|
432
|
-
|
|
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
|
|
433
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)
|
pywrangler/types.py
CHANGED
|
@@ -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]
|
pywrangler/utils.py
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import re
|
|
2
3
|
import subprocess
|
|
4
|
+
import tomllib
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from functools import cache
|
|
3
7
|
from pathlib import Path
|
|
8
|
+
from typing import Literal, TypedDict, cast
|
|
4
9
|
|
|
5
10
|
import click
|
|
6
|
-
|
|
11
|
+
import pyjson5
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.logging import RichHandler
|
|
7
14
|
from rich.theme import Theme
|
|
8
15
|
|
|
16
|
+
from .metadata import PYTHON_COMPAT_VERSIONS
|
|
17
|
+
|
|
9
18
|
WRANGLER_COMMAND = ["npx", "--yes", "wrangler"]
|
|
10
19
|
WRANGLER_CREATE_COMMAND = ["npx", "--yes", "create-cloudflare"]
|
|
11
20
|
|
|
@@ -16,7 +25,7 @@ RUNNING_LEVEL = 15
|
|
|
16
25
|
OUTPUT_LEVEL = 16
|
|
17
26
|
|
|
18
27
|
|
|
19
|
-
def setup_logging():
|
|
28
|
+
def setup_logging() -> None:
|
|
20
29
|
console = Console(
|
|
21
30
|
theme=Theme(
|
|
22
31
|
{
|
|
@@ -44,17 +53,17 @@ def setup_logging():
|
|
|
44
53
|
logging.addLevelName(OUTPUT_LEVEL, "OUTPUT")
|
|
45
54
|
|
|
46
55
|
|
|
47
|
-
def write_success(msg):
|
|
56
|
+
def write_success(msg: str) -> None:
|
|
48
57
|
logging.log(SUCCESS_LEVEL, msg)
|
|
49
58
|
|
|
50
59
|
|
|
51
60
|
def run_command(
|
|
52
61
|
command: list[str | Path],
|
|
53
62
|
cwd: Path | None = None,
|
|
54
|
-
env: dict | None = None,
|
|
63
|
+
env: dict[str, str | Path] | None = None,
|
|
55
64
|
check: bool = True,
|
|
56
65
|
capture_output: bool = False,
|
|
57
|
-
):
|
|
66
|
+
) -> subprocess.CompletedProcess[str]:
|
|
58
67
|
"""
|
|
59
68
|
Runs a command and handles logging and errors.
|
|
60
69
|
|
|
@@ -70,27 +79,32 @@ def run_command(
|
|
|
70
79
|
"""
|
|
71
80
|
logger.log(RUNNING_LEVEL, f"{' '.join(str(arg) for arg in command)}")
|
|
72
81
|
try:
|
|
82
|
+
kwargs = {}
|
|
83
|
+
if capture_output:
|
|
84
|
+
kwargs = dict(stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
85
|
+
|
|
73
86
|
process = subprocess.run(
|
|
74
87
|
command,
|
|
75
88
|
cwd=cwd,
|
|
76
89
|
env=env,
|
|
77
90
|
check=check,
|
|
78
|
-
capture_output=capture_output,
|
|
79
91
|
text=True,
|
|
80
|
-
|
|
92
|
+
**kwargs,
|
|
93
|
+
) # type: ignore[call-overload]
|
|
81
94
|
if process.stdout and not capture_output:
|
|
82
95
|
logger.log(OUTPUT_LEVEL, f"{process.stdout.strip()}")
|
|
83
|
-
return process
|
|
96
|
+
return process # type: ignore[no-any-return]
|
|
84
97
|
except subprocess.CalledProcessError as e:
|
|
85
98
|
logger.error(
|
|
86
|
-
f"Error running command: {' '.join(str(arg) for arg in command)}\nExit code: {e.returncode}\nOutput:\n{e.stdout.strip() if e.stdout else ''}
|
|
99
|
+
f"Error running command: {' '.join(str(arg) for arg in command)}\nExit code: {e.returncode}\nOutput:\n{e.stdout.strip() if e.stdout else ''}"
|
|
87
100
|
)
|
|
88
|
-
raise click.exceptions.Exit(code=e.returncode)
|
|
101
|
+
raise click.exceptions.Exit(code=e.returncode) from None
|
|
89
102
|
except FileNotFoundError:
|
|
90
103
|
logger.error(f"Command not found: {command[0]}. Is it installed and in PATH?")
|
|
91
|
-
raise click.exceptions.Exit(code=1)
|
|
104
|
+
raise click.exceptions.Exit(code=1) from None
|
|
92
105
|
|
|
93
106
|
|
|
107
|
+
@cache
|
|
94
108
|
def find_pyproject_toml() -> Path:
|
|
95
109
|
"""
|
|
96
110
|
Search for pyproject.toml starting from current working directory and going up the directory tree.
|
|
@@ -112,3 +126,200 @@ def find_pyproject_toml() -> Path:
|
|
|
112
126
|
f"pyproject.toml not found in {Path.cwd().resolve()} or any parent directories"
|
|
113
127
|
)
|
|
114
128
|
raise click.exceptions.Exit(code=1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class PyProjectProject(TypedDict):
|
|
132
|
+
dependencies: list[str]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class PyProject(TypedDict):
|
|
136
|
+
project: PyProjectProject
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def read_pyproject_toml() -> PyProject:
|
|
140
|
+
pyproject_toml = find_pyproject_toml()
|
|
141
|
+
logger.debug(f"Reading {pyproject_toml}...")
|
|
142
|
+
try:
|
|
143
|
+
with open(pyproject_toml, "rb") as f:
|
|
144
|
+
return cast(PyProject, tomllib.load(f))
|
|
145
|
+
except tomllib.TOMLDecodeError as e:
|
|
146
|
+
logger.error(f"Error parsing {pyproject_toml}: {str(e)}")
|
|
147
|
+
raise click.exceptions.Exit(code=1) from None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_project_root() -> Path:
|
|
151
|
+
return find_pyproject_toml().parent
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
MIN_UV_VERSION = (0, 8, 10)
|
|
155
|
+
MIN_WRANGLER_VERSION = (4, 42, 1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def check_uv_version() -> None:
|
|
159
|
+
res = run_command(["uv", "--version"], capture_output=True)
|
|
160
|
+
ver_str = res.stdout.split(" ")[1]
|
|
161
|
+
ver = tuple(int(x) for x in ver_str.split("."))
|
|
162
|
+
if ver >= MIN_UV_VERSION:
|
|
163
|
+
return
|
|
164
|
+
min_version_str = ".".join(str(x) for x in MIN_UV_VERSION)
|
|
165
|
+
logger.error(f"uv version at least {min_version_str} required, have {ver_str}.")
|
|
166
|
+
logger.error("Update uv with `uv self update`.")
|
|
167
|
+
raise click.exceptions.Exit(code=1)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def check_wrangler_version() -> None:
|
|
171
|
+
"""
|
|
172
|
+
Check that the installed wrangler version is at least 4.42.1.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
click.exceptions.Exit: If wrangler is not installed or version is too old.
|
|
176
|
+
"""
|
|
177
|
+
result = run_command(
|
|
178
|
+
["npx", "--yes", "wrangler", "--version"], capture_output=True, check=False
|
|
179
|
+
)
|
|
180
|
+
if result.returncode != 0:
|
|
181
|
+
logger.error("Failed to get wrangler version. Is wrangler installed?")
|
|
182
|
+
logger.error("Install wrangler with: npm install wrangler@latest")
|
|
183
|
+
raise click.exceptions.Exit(code=1)
|
|
184
|
+
|
|
185
|
+
# Parse version from output like "wrangler 4.42.1" or " ⛅️ wrangler 4.42.1"
|
|
186
|
+
version_line = result.stdout.strip()
|
|
187
|
+
# Extract version number using regex
|
|
188
|
+
version_match = re.search(r"(\d+)\.(\d+)\.(\d+)", version_line)
|
|
189
|
+
|
|
190
|
+
if not version_match:
|
|
191
|
+
logger.error(f"Could not parse wrangler version from: {version_line}")
|
|
192
|
+
logger.error("Install wrangler with: npm install wrangler@latest")
|
|
193
|
+
raise click.exceptions.Exit(code=1)
|
|
194
|
+
|
|
195
|
+
major, minor, patch = map(int, version_match.groups())
|
|
196
|
+
current_version = (major, minor, patch)
|
|
197
|
+
|
|
198
|
+
if current_version < MIN_WRANGLER_VERSION:
|
|
199
|
+
min_version_str = ".".join(str(x) for x in MIN_WRANGLER_VERSION)
|
|
200
|
+
current_version_str = ".".join(str(x) for x in current_version)
|
|
201
|
+
logger.error(
|
|
202
|
+
f"wrangler version at least {min_version_str} required, have {current_version_str}."
|
|
203
|
+
)
|
|
204
|
+
logger.error("Update wrangler with: npm install wrangler@latest")
|
|
205
|
+
raise click.exceptions.Exit(code=1)
|
|
206
|
+
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"wrangler version {'.'.join(str(x) for x in current_version)} is sufficient"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def check_wrangler_config() -> None:
|
|
213
|
+
PROJECT_ROOT = get_project_root()
|
|
214
|
+
wrangler_jsonc = PROJECT_ROOT / "wrangler.jsonc"
|
|
215
|
+
wrangler_toml = PROJECT_ROOT / "wrangler.toml"
|
|
216
|
+
if not wrangler_jsonc.is_file() and not wrangler_toml.is_file():
|
|
217
|
+
logger.error(
|
|
218
|
+
f"{wrangler_jsonc} or {wrangler_toml} not found in {PROJECT_ROOT}."
|
|
219
|
+
)
|
|
220
|
+
raise click.exceptions.Exit(code=1)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class WranglerConfig(TypedDict, total=False):
|
|
224
|
+
compatibility_date: str
|
|
225
|
+
compatibility_flags: list[str]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _parse_wrangler_config() -> WranglerConfig:
|
|
229
|
+
"""
|
|
230
|
+
Parse wrangler configuration from either wrangler.toml or wrangler.jsonc.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
dict: Parsed configuration data
|
|
234
|
+
"""
|
|
235
|
+
PROJECT_ROOT = get_project_root()
|
|
236
|
+
wrangler_toml = PROJECT_ROOT / "wrangler.toml"
|
|
237
|
+
wrangler_jsonc = PROJECT_ROOT / "wrangler.jsonc"
|
|
238
|
+
|
|
239
|
+
if wrangler_toml.is_file():
|
|
240
|
+
try:
|
|
241
|
+
with open(wrangler_toml, "rb") as f:
|
|
242
|
+
return cast(WranglerConfig, tomllib.load(f))
|
|
243
|
+
except tomllib.TOMLDecodeError as e:
|
|
244
|
+
logger.error(f"Error parsing {wrangler_toml}: {e}")
|
|
245
|
+
raise click.exceptions.Exit(code=1) from None
|
|
246
|
+
|
|
247
|
+
if wrangler_jsonc.is_file():
|
|
248
|
+
try:
|
|
249
|
+
with open(wrangler_jsonc) as f:
|
|
250
|
+
content = f.read()
|
|
251
|
+
return cast(WranglerConfig, pyjson5.loads(content))
|
|
252
|
+
except (pyjson5.Json5DecoderException, ValueError) as e:
|
|
253
|
+
logger.error(f"Error parsing {wrangler_jsonc}: {e}")
|
|
254
|
+
raise click.exceptions.Exit(code=1) from None
|
|
255
|
+
|
|
256
|
+
return {}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@cache
|
|
260
|
+
def get_python_version() -> Literal["3.12", "3.13"]:
|
|
261
|
+
"""
|
|
262
|
+
Determine Python version from wrangler configuration.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Python version string
|
|
266
|
+
"""
|
|
267
|
+
config = _parse_wrangler_config()
|
|
268
|
+
|
|
269
|
+
if not config:
|
|
270
|
+
logger.error("No wrangler config found")
|
|
271
|
+
raise click.exceptions.Exit(code=1)
|
|
272
|
+
|
|
273
|
+
compat_flags = config.get("compatibility_flags", [])
|
|
274
|
+
compat_date_str = config.get("compatibility_date", None)
|
|
275
|
+
if compat_date_str is None:
|
|
276
|
+
logger.error("No compatibility_date specified in wrangler config")
|
|
277
|
+
raise click.exceptions.Exit(code=1)
|
|
278
|
+
try:
|
|
279
|
+
compat_date = datetime.strptime(compat_date_str, "%Y-%m-%d")
|
|
280
|
+
except ValueError:
|
|
281
|
+
logger.error(
|
|
282
|
+
f"Invalid compatibility_date format: {config.get('compatibility_date')}"
|
|
283
|
+
)
|
|
284
|
+
raise click.exceptions.Exit(code=1) from None
|
|
285
|
+
|
|
286
|
+
# Check if python_workers base flag is present (required for Python workers)
|
|
287
|
+
if "python_workers" not in compat_flags:
|
|
288
|
+
logger.error("`python_workers` compat flag not specified in wrangler config")
|
|
289
|
+
raise click.exceptions.Exit(code=1)
|
|
290
|
+
|
|
291
|
+
# Find the most specific Python version based on compat flags and date
|
|
292
|
+
# Sort by version descending to prioritize newer versions
|
|
293
|
+
sorted_versions = sorted(
|
|
294
|
+
PYTHON_COMPAT_VERSIONS, key=lambda x: x.version, reverse=True
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
for py_version in sorted_versions:
|
|
298
|
+
# Check if the specific compat flag is present
|
|
299
|
+
if py_version.compat_flag in compat_flags:
|
|
300
|
+
return py_version.version
|
|
301
|
+
|
|
302
|
+
# For versions with compat_date, also check the date requirement
|
|
303
|
+
if py_version.compat_date and compat_date >= py_version.compat_date:
|
|
304
|
+
return py_version.version
|
|
305
|
+
|
|
306
|
+
logger.error("Could not determine Python version from wrangler config")
|
|
307
|
+
raise click.exceptions.Exit(code=1)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def get_uv_pyodide_interp_name() -> str:
|
|
311
|
+
match get_python_version():
|
|
312
|
+
case "3.12":
|
|
313
|
+
v = "3.12.7"
|
|
314
|
+
case "3.13":
|
|
315
|
+
v = "3.13.2"
|
|
316
|
+
return f"cpython-{v}-emscripten-wasm32-musl"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def get_pyodide_index() -> str:
|
|
320
|
+
match get_python_version():
|
|
321
|
+
case "3.12":
|
|
322
|
+
v = "0.27.7"
|
|
323
|
+
case "3.13":
|
|
324
|
+
v = "0.28.3"
|
|
325
|
+
return "https://index.pyodide.org/" + v
|
|
@@ -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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pywrangler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pywrangler/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
3
|
+
pywrangler/cli.py,sha256=8C4iBntruyMkQqOb0KfH_H0QNGYYWEBtKFA7b7zGSmE,5467
|
|
4
|
+
pywrangler/metadata.py,sha256=ndh584ALzshSXwduTmqVczfF5Mpn7z0F9ztn3Dugf70,408
|
|
5
|
+
pywrangler/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
pywrangler/sync.py,sha256=TuM_bmBdgi37xYFtD62GTNwyNaDgO87QwfS-OYLR8OM,11647
|
|
7
|
+
pywrangler/types.py,sha256=hYJ6hNIjWb0faBGW82AWfCBsF_JPb7sXKCXPtFM3Mhk,1183
|
|
8
|
+
pywrangler/utils.py,sha256=mPY8LcRqYMuyVzyWS_077-4MX3K6sIThV1gY8_br9U0,10495
|
|
9
|
+
workers_py-1.7.0.dist-info/METADATA,sha256=z6R2A_5LLb2hE5xY6NOpqXxX0ooA3kexOp1BIho7qlc,1774
|
|
10
|
+
workers_py-1.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
workers_py-1.7.0.dist-info/entry_points.txt,sha256=pt6X-Nv5-gSiKUwrnvLwzlSXs9yP37m7zdTAi8f6nAM,50
|
|
12
|
+
workers_py-1.7.0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
pywrangler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
pywrangler/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
3
|
-
pywrangler/cli.py,sha256=voUaP2tHCwxznzZ45O6ZAJ91vbp6q9UNb7H2OlgnZ_k,6596
|
|
4
|
-
pywrangler/metadata.py,sha256=vttmaCtouSr9ADj8ncvNGqeaWEGFP8pamH2T6ohFjnA,408
|
|
5
|
-
pywrangler/sync.py,sha256=fFavYhbvBXF4memxrJLnMJpApET_3fx9NmJ_O6NDzxw,14217
|
|
6
|
-
pywrangler/types.py,sha256=RyOTVFFC75vJ0NxGzXWJGouRpQ5k4gsf3Jsiq-i_ans,1174
|
|
7
|
-
pywrangler/utils.py,sha256=JOPCXy_O_RZ-ClSSWL5pel2Iltyclj_JV13IxKr_WRo,3382
|
|
8
|
-
workers_py-1.6.1.dist-info/METADATA,sha256=0oipVsEgOgZhdUXJp9ckB7HWqGVvqqsh2sbcQBL1yIY,1732
|
|
9
|
-
workers_py-1.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
-
workers_py-1.6.1.dist-info/entry_points.txt,sha256=pt6X-Nv5-gSiKUwrnvLwzlSXs9yP37m7zdTAi8f6nAM,50
|
|
11
|
-
workers_py-1.6.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|