shipit-cli 0.3.4__tar.gz → 0.4.1__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.
Files changed (24) hide show
  1. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/PKG-INFO +1 -1
  2. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/pyproject.toml +1 -1
  3. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/cli.py +45 -6
  4. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/generator.py +6 -1
  5. shipit_cli-0.4.1/src/shipit/providers/python.py +310 -0
  6. shipit_cli-0.4.1/src/shipit/version.py +5 -0
  7. shipit_cli-0.3.4/src/shipit/providers/python.py +0 -141
  8. shipit_cli-0.3.4/src/shipit/version.py +0 -5
  9. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/.gitignore +0 -0
  10. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/README.md +0 -0
  11. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/__init__.py +0 -0
  12. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/assets/php/php.ini +0 -0
  13. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/providers/base.py +0 -0
  14. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/providers/gatsby.py +0 -0
  15. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/providers/hugo.py +0 -0
  16. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/providers/laravel.py +0 -0
  17. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/providers/mkdocs.py +0 -0
  18. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/providers/node_static.py +0 -0
  19. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/providers/php.py +0 -0
  20. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/providers/registry.py +0 -0
  21. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/src/shipit/providers/staticfile.py +0 -0
  22. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/tests/test_examples_build.py +0 -0
  23. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/tests/test_generate_shipit_examples.py +0 -0
  24. {shipit_cli-0.3.4 → shipit_cli-0.4.1}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipit-cli
3
- Version: 0.3.4
3
+ Version: 0.4.1
4
4
  Summary: Add your description here
5
5
  Project-URL: homepage, https://wasmer.io
6
6
  Project-URL: repository, https://github.com/wasmerio/shipit
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shipit-cli"
3
- version = "0.3.4"
3
+ version = "0.4.1"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -55,6 +55,7 @@ class Serve:
55
55
  build: List["Step"]
56
56
  deps: List["Package"]
57
57
  commands: Dict[str, str]
58
+ cwd: Optional[str] = None
58
59
  assets: Optional[Dict[str, str]] = None
59
60
  prepare: Optional[List["PrepareStep"]] = None
60
61
  workers: Optional[List[str]] = None
@@ -577,18 +578,33 @@ class LocalBuilder:
577
578
  self.create_file(asset_path, assets[asset])
578
579
 
579
580
  def build_prepare(self, serve: Serve) -> None:
580
- app_dir = self.get_build_path()
581
581
  self.prepare_bash_script.parent.mkdir(parents=True, exist_ok=True)
582
582
  commands: List[str] = []
583
+ if serve.cwd:
584
+ commands.append(f"cd {serve.cwd}")
583
585
  if serve.prepare:
584
586
  for step in serve.prepare:
585
587
  if isinstance(step, RunStep):
586
588
  commands.append(step.command)
587
589
  elif isinstance(step, WorkdirStep):
588
590
  commands.append(f"cd {step.path}")
589
- content = "#!/bin/bash\ncd {app_dir}\n{body}".format(
590
- app_dir=app_dir, body="\n".join(commands)
591
+ content = "#!/bin/bash\n{body}".format(
592
+ body="\n".join(commands)
591
593
  )
594
+ console.print(f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]")
595
+ manifest_panel = Panel(
596
+ Syntax(
597
+ content,
598
+ "bash",
599
+ theme="monokai",
600
+ background_color="default",
601
+ line_numbers=True,
602
+ ),
603
+ box=box.SQUARE,
604
+ border_style="bright_black",
605
+ expand=False,
606
+ )
607
+ console.print(manifest_panel, markup=False, highlight=True)
592
608
  self.prepare_bash_script.write_text(content)
593
609
  self.prepare_bash_script.chmod(0o755)
594
610
 
@@ -739,15 +755,35 @@ class WasmerBuilder:
739
755
  env_lines = ""
740
756
 
741
757
  commands: List[str] = []
758
+ if serve.cwd:
759
+ commands.append(f"cd {serve.cwd}")
760
+
742
761
  if serve.prepare:
743
762
  for step in serve.prepare:
744
763
  if isinstance(step, RunStep):
745
764
  commands.append(step.command)
746
765
  elif isinstance(step, WorkdirStep):
747
766
  commands.append(f"cd {step.path}")
767
+
748
768
  body = "\n".join(filter(None, [env_lines, *commands]))
769
+ content = f"#!/bin/bash\n\n{body}"
770
+ console.print(f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]")
771
+ manifest_panel = Panel(
772
+ Syntax(
773
+ content,
774
+ "bash",
775
+ theme="monokai",
776
+ background_color="default",
777
+ line_numbers=True,
778
+ ),
779
+ box=box.SQUARE,
780
+ border_style="bright_black",
781
+ expand=False,
782
+ )
783
+ console.print(manifest_panel, markup=False, highlight=True)
784
+
749
785
  (prepare_dir / "prepare.sh").write_text(
750
- f"#!/bin/bash\n\n{body}",
786
+ content,
751
787
  )
752
788
  (prepare_dir / "prepare.sh").chmod(0o755)
753
789
 
@@ -842,7 +878,8 @@ class WasmerBuilder:
842
878
  command.add("module", program_binary["script"])
843
879
  command.add("runner", "wasi")
844
880
  wasi_args = table()
845
- wasi_args.add("cwd", "/app")
881
+ if serve.cwd:
882
+ wasi_args.add("cwd", serve.cwd)
846
883
  wasi_args.add("main-args", parts[1:])
847
884
  env = program_binary.get("env") or {}
848
885
  if serve.env:
@@ -1024,6 +1061,7 @@ class Ctx:
1024
1061
  build: List[str],
1025
1062
  deps: List[str],
1026
1063
  commands: Dict[str, str],
1064
+ cwd: Optional[str] = None,
1027
1065
  assets: Optional[Dict[str, str]] = None,
1028
1066
  prepare: Optional[List[str]] = None,
1029
1067
  workers: Optional[List[str]] = None,
@@ -1043,6 +1081,7 @@ class Ctx:
1043
1081
  name=name,
1044
1082
  provider=provider,
1045
1083
  build=build_refs,
1084
+ cwd=cwd,
1046
1085
  assets=assets,
1047
1086
  deps=dep_refs,
1048
1087
  commands=commands,
@@ -1433,7 +1472,7 @@ def main() -> None:
1433
1472
  app()
1434
1473
  except Exception as e:
1435
1474
  console.print(f"[bold red]{type(e).__name__}[/bold red]: {e}")
1436
- raise e
1475
+ # raise e
1437
1476
 
1438
1477
 
1439
1478
  if __name__ == "__main__":
@@ -115,8 +115,10 @@ def generate_shipit(path: Path) -> str:
115
115
  env_lines = ",\n".join([f' "{k}": {v}' for k, v in plan.env.items()])
116
116
  assets_block = _render_assets(plan.assets)
117
117
  mounts_block = None
118
+ attach_serve_names: list[str] = []
118
119
  if plan.mounts:
119
- mounts = filter(lambda m: m.attach_to_serve, plan.mounts)
120
+ mounts = list(filter(lambda m: m.attach_to_serve, plan.mounts))
121
+ attach_serve_names = [m.name for m in mounts]
120
122
  mounts_block = ",\n".join([f" {m.name}" for m in mounts])
121
123
 
122
124
  out: List[str] = []
@@ -131,6 +133,9 @@ def generate_shipit(path: Path) -> str:
131
133
  out.append("serve(")
132
134
  out.append(f' name="{plan.serve_name}",')
133
135
  out.append(f' provider="{plan.provider}",')
136
+ # If app is mounted for serve, set cwd to the app serve path
137
+ if "app" in attach_serve_names:
138
+ out.append(' cwd=app["serve"],')
134
139
  out.append(" build=[")
135
140
  out.append(build_steps_block)
136
141
  out.append(" ],")
@@ -0,0 +1,310 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Dict, Optional, Set
6
+ from enum import Enum
7
+
8
+ from .base import (
9
+ DetectResult,
10
+ DependencySpec,
11
+ Provider,
12
+ _exists,
13
+ MountSpec,
14
+ )
15
+
16
+
17
+ class PythonFramework(Enum):
18
+ Django = "django"
19
+ FastAPI = "fastapi"
20
+ Flask = "flask"
21
+ FastHTML = "python-fasthtml"
22
+
23
+
24
+ class PythonServer(Enum):
25
+ Hypercorn = "hypercorn"
26
+ Uvicorn = "uvicorn"
27
+ # Gunicorn = "gunicorn"
28
+ Daphne = "daphne"
29
+
30
+ class DatabaseType(Enum):
31
+ MySQL = "mysql"
32
+ PostgreSQL = "postgresql"
33
+
34
+
35
+ class PythonProvider:
36
+ framework: Optional[PythonFramework] = None
37
+ server: Optional[PythonServer] = None
38
+ database: Optional[DatabaseType] = None
39
+ extra_dependencies: Set[str]
40
+ asgi_application: Optional[str] = None
41
+ wsgi_application: Optional[str] = None
42
+
43
+ def __init__(self, path: Path):
44
+ self.path = path
45
+ if _exists(self.path, ".python-version"):
46
+ python_version = (self.path / ".python-version").read_text().strip()
47
+ else:
48
+ python_version = "3.13"
49
+ self.default_python_version = python_version
50
+ self.extra_dependencies = set()
51
+
52
+ pg_deps = {
53
+ "asyncpg",
54
+ "aiopg",
55
+ "psycopg",
56
+ "psycopg2",
57
+ "psycopg-binary",
58
+ "psycopg2-binary"}
59
+ mysql_deps = {"mysqlclient", "pymysql", "mysql-connector-python", "aiomysql"}
60
+ found_deps = self.check_deps(
61
+ "django",
62
+ "fastapi",
63
+ "flask",
64
+ "python-fasthtml",
65
+ "daphne",
66
+ "hypercorn",
67
+ "uvicorn",
68
+ # "gunicorn",
69
+ *mysql_deps,
70
+ *pg_deps,
71
+ )
72
+
73
+ # ASGI/WSGI Server
74
+ if "uvicorn" in found_deps:
75
+ server = PythonServer.Uvicorn
76
+ elif "hypercorn" in found_deps:
77
+ server = PythonServer.Hypercorn
78
+ # elif "gunicorn" in found_deps:
79
+ # server = PythonServer.Gunicorn
80
+ elif "daphne" in found_deps:
81
+ server = PythonServer.Daphne
82
+ else:
83
+ server = None
84
+ self.server = server
85
+
86
+ # Set framework
87
+ if _exists(self.path, "manage.py") and ("django" in found_deps):
88
+ framework = PythonFramework.Django
89
+ # Find the settings.py file using glob
90
+ settings_file = next(self.path.glob( "**/settings.py"))
91
+ if settings_file:
92
+ asgi_match = re.search(r"ASGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text())
93
+ if asgi_match:
94
+ self.asgi_application = asgi_match.group(1)
95
+ else:
96
+ wsgi_match = re.search(r"WSGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text())
97
+ if wsgi_match:
98
+ self.wsgi_application = wsgi_match.group(1)
99
+
100
+ if not self.server:
101
+ if self.asgi_application:
102
+ self.extra_dependencies = {"uvicorn"}
103
+ self.server = PythonServer.Uvicorn
104
+ elif self.wsgi_application:
105
+ # gunicorn can't run with Wasmer atm
106
+ self.extra_dependencies = {"uvicorn"}
107
+ self.server = PythonServer.Uvicorn
108
+ elif "fastapi" in found_deps:
109
+ framework = PythonFramework.FastAPI
110
+ if not self.server:
111
+ self.extra_dependencies = {"uvicorn"}
112
+ self.server = PythonServer.Uvicorn
113
+ elif "flask" in found_deps:
114
+ framework = PythonFramework.Flask
115
+ elif "fastapi" in found_deps:
116
+ framework = PythonFramework.FastAPI
117
+ elif "flask" in found_deps:
118
+ framework = PythonFramework.Flask
119
+ elif "python-fasthtml" in found_deps:
120
+ framework = PythonFramework.FastHTML
121
+ else:
122
+ framework = None
123
+ self.framework = framework
124
+
125
+ # Database
126
+ if mysql_deps & found_deps:
127
+ database = DatabaseType.MySQL
128
+ elif pg_deps & found_deps:
129
+ database = DatabaseType.PostgreSQL
130
+ else:
131
+ database = None
132
+ self.database = database
133
+
134
+ def check_deps(self, *deps: str) -> Set[str]:
135
+ deps = set([dep.lower() for dep in deps])
136
+ initial_deps = set(deps)
137
+ for file in ["requirements.txt", "pyproject.toml"]:
138
+ if _exists(self.path, file):
139
+ for line in (self.path / file).read_text().splitlines():
140
+ for dep in set(deps):
141
+ if dep in line.lower():
142
+ deps.remove(dep)
143
+ if not deps:
144
+ break
145
+ if not deps:
146
+ break
147
+ if not deps:
148
+ break
149
+ return initial_deps-deps
150
+
151
+ @classmethod
152
+ def name(cls) -> str:
153
+ return "python"
154
+
155
+ @classmethod
156
+ def detect(cls, path: Path) -> Optional[DetectResult]:
157
+ if _exists(path, "pyproject.toml", "requirements.txt"):
158
+ if _exists(path, "manage.py"):
159
+ return DetectResult(cls.name(), 70)
160
+ return DetectResult(cls.name(), 50)
161
+ return None
162
+
163
+ def initialize(self) -> None:
164
+ pass
165
+
166
+ def serve_name(self) -> str:
167
+ return self.path.name
168
+
169
+ def provider_kind(self) -> str:
170
+ return "python"
171
+
172
+ def dependencies(self) -> list[DependencySpec]:
173
+ return [
174
+ DependencySpec(
175
+ "python",
176
+ env_var="SHIPIT_PYTHON_VERSION",
177
+ default_version=self.default_python_version,
178
+ use_in_build=True,
179
+ use_in_serve=True,
180
+ ),
181
+ DependencySpec(
182
+ "uv",
183
+ env_var="SHIPIT_UV_VERSION",
184
+ default_version="0.8.15",
185
+ use_in_build=True,
186
+ ),
187
+ ]
188
+
189
+ def declarations(self) -> Optional[str]:
190
+ return (
191
+ "cross_platform = getenv(\"SHIPIT_PYTHON_CROSS_PLATFORM\")\n"
192
+ "python_extra_index_url = getenv(\"SHIPIT_PYTHON_EXTRA_INDEX_URL\")\n"
193
+ "precompile_python = getenv(\"SHIPIT_PYTHON_PRECOMPILE\") in [\"true\", \"True\", \"TRUE\", \"1\", \"on\", \"yes\", \"y\", \"Y\", \"YES\", \"On\", \"ON\"]\n"
194
+ "python_cross_packages_path = venv[\"build\"] + f\"/lib/python{python_version}/site-packages\"\n"
195
+ "python_serve_path = \"{}/lib/python{}/site-packages\".format(venv[\"serve\"], python_version)\n"
196
+ )
197
+
198
+ def build_steps(self) -> list[str]:
199
+ steps = [
200
+ "workdir(app[\"build\"])"
201
+ ]
202
+
203
+ if _exists(self.path, "pyproject.toml"):
204
+ input_files = ["pyproject.toml"]
205
+ extra_args = ""
206
+ if _exists(self.path, "uv.lock"):
207
+ input_files.append("uv.lock")
208
+ extra_args = " --locked"
209
+ inputs = ", ".join([f"\"{input}\"" for input in input_files])
210
+ extra_deps = ", ".join([f"{dep}" for dep in self.extra_dependencies])
211
+ steps += list(filter(None, [
212
+ "env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
213
+ "run(f\"uv sync --compile --python python{python_version} --no-managed-python" + extra_args + "\", inputs=[" + inputs + "], group=\"install\")",
214
+ "copy(\"pyproject.toml\", \"pyproject.toml\")",
215
+ f"run(\"uv add {extra_deps}\", group=\"install\")" if extra_deps else None,
216
+ "run(f\"uv pip compile pyproject.toml --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --only-binary :all: -o cross-requirements.txt\", outputs=[\"cross-requirements.txt\"]) if cross_platform else None",
217
+ f"run(f\"uvx pip install -r cross-requirements.txt {extra_deps} --target {{python_cross_packages_path}} --platform {{cross_platform}} --only-binary=:all: --python-version={{python_version}} --compile\") if cross_platform else None",
218
+ "run(\"rm cross-requirements.txt\") if cross_platform else None",
219
+ ]))
220
+ if _exists(self.path, "requirements.txt"):
221
+ steps += [
222
+ "env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
223
+ "run(f\"uv init --no-managed-python --python python{python_version}\", inputs=[], outputs=[\"uv.lock\"], group=\"install\")",
224
+ "run(f\"uv add -r requirements.txt\", inputs=[\"requirements.txt\"], group=\"install\")",
225
+ "run(f\"uv pip compile requirements.txt --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --only-binary :all: -o cross-requirements.txt\", inputs=[\"requirements.txt\"], outputs=[\"cross-requirements.txt\"]) if cross_platform else None",
226
+ "run(f\"uvx pip install -r cross-requirements.txt --target {python_cross_packages_path} --platform {cross_platform} --only-binary=:all: --python-version={python_version} --compile\") if cross_platform else None",
227
+ "run(\"rm cross-requirements.txt\") if cross_platform else None",
228
+ ]
229
+
230
+ steps += [
231
+ "path((local_venv[\"build\"] if cross_platform else venv[\"build\"]) + \"/bin\")",
232
+ "copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
233
+ ]
234
+ return steps
235
+
236
+ def prepare_steps(self) -> Optional[list[str]]:
237
+ return [
238
+ 'run("echo \\\"Precompiling Python code...\\\"") if precompile_python else None',
239
+ 'run(f"python -m compileall -o 2 {python_serve_path}") if precompile_python else None',
240
+ 'run("echo \\\"Precompiling package code...\\\"") if precompile_python else None',
241
+ 'run("python -m compileall -o 2 {}".format(app["serve"])) if precompile_python else None',
242
+ ]
243
+
244
+ def commands(self) -> Dict[str, str]:
245
+ if self.framework == PythonFramework.Django:
246
+ start_cmd = None
247
+ if self.server == PythonServer.Daphne and self.asgi_application:
248
+ asgi_application = format_app_import(self.asgi_application)
249
+ start_cmd = f'"python -m daphne {asgi_application} --bind 0.0.0.0 --port 8000"'
250
+ elif self.server == PythonServer.Uvicorn:
251
+ if self.asgi_application:
252
+ asgi_application = format_app_import(self.asgi_application)
253
+ start_cmd = f'"python -m uvicorn {asgi_application} --host 0.0.0.0 --port 8000"'
254
+ elif self.wsgi_application:
255
+ wsgi_application = format_app_import(self.wsgi_application)
256
+ start_cmd = f'"python -m uvicorn {wsgi_application} --interface=wsgi --host 0.0.0.0 --port 8000"'
257
+ # elif self.server == PythonServer.Gunicorn:
258
+ # start_cmd = f'"python -m gunicorn {self.wsgi_application} --bind 0.0.0.0 --port 8000"'
259
+ if not start_cmd:
260
+ # We run the default runserver command if no server is specified
261
+ start_cmd = '"python manage.py runserver 0.0.0.0:8000"'
262
+ migrate_cmd = '"python manage.py migrate"'
263
+ return {"start": start_cmd, "after_deploy": migrate_cmd}
264
+ elif self.framework == PythonFramework.FastAPI:
265
+ if _exists(self.path, "main.py"):
266
+ path = "main:app"
267
+ elif _exists(self.path, "src/main.py"):
268
+ path = "src.main:app"
269
+
270
+ if self.server == PythonServer.Uvicorn:
271
+ start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
272
+ elif self.server == PythonServer.Hypercorn:
273
+ start_cmd = f'"python -m hypercorn {path} --bind 0.0.0.0:8000"'
274
+ else:
275
+ start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
276
+ return {"start": start_cmd}
277
+ elif self.framework == PythonFramework.FastHTML:
278
+ if _exists(self.path, "main.py"):
279
+ path = "main:app"
280
+ elif _exists(self.path, "src/main.py"):
281
+ path = "src.main:app"
282
+ start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
283
+ elif _exists(self.path, "main.py"):
284
+ start_cmd = '"python main.py"'
285
+ elif _exists(self.path, "src/main.py"):
286
+ start_cmd = '"python src/main.py"'
287
+ else:
288
+ start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
289
+ return {"start": start_cmd}
290
+
291
+ def assets(self) -> Optional[Dict[str, str]]:
292
+ return None
293
+
294
+ def mounts(self) -> list[MountSpec]:
295
+ return [
296
+ MountSpec("app"),
297
+ MountSpec("venv"),
298
+ MountSpec("local_venv", attach_to_serve=False),
299
+ ]
300
+
301
+ def env(self) -> Optional[Dict[str, str]]:
302
+ # For Django projects, generate an empty env dict to surface the field
303
+ # in the Shipit file. Other Python projects omit it by default.
304
+ return {
305
+ "PYTHONPATH": "python_serve_path"
306
+ }
307
+
308
+ def format_app_import(asgi_application: str) -> str:
309
+ # Transform "mysite.asgi.application" to "mysite.asgi:application" using regex
310
+ return re.sub(r"\.([^.]+)$", r":\1", asgi_application)
@@ -0,0 +1,5 @@
1
+ __all__ = ["version", "version_info"]
2
+
3
+
4
+ version = "0.4.1"
5
+ version_info = (0, 4, 1, "final", 0)
@@ -1,141 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from typing import Dict, Optional
5
-
6
- from .base import (
7
- DetectResult,
8
- DependencySpec,
9
- Provider,
10
- _exists,
11
- MountSpec,
12
- )
13
-
14
-
15
- class PythonProvider:
16
- def __init__(self, path: Path):
17
- self.path = path
18
- @classmethod
19
- def name(cls) -> str:
20
- return "python"
21
-
22
- @classmethod
23
- def detect(cls, path: Path) -> Optional[DetectResult]:
24
- if _exists(path, "pyproject.toml", "requirements.txt"):
25
- if _exists(path, "manage.py"):
26
- return DetectResult(cls.name(), 70)
27
- return DetectResult(cls.name(), 50)
28
- return None
29
-
30
- def initialize(self) -> None:
31
- pass
32
-
33
- def serve_name(self) -> str:
34
- return self.path.name
35
-
36
- def provider_kind(self) -> str:
37
- return "python"
38
-
39
- def dependencies(self) -> list[DependencySpec]:
40
- if _exists(self.path, ".python-version"):
41
- python_version = (self.path / ".python-version").read_text().strip()
42
- else:
43
- python_version = "3.13"
44
-
45
- return [
46
- DependencySpec(
47
- "python",
48
- env_var="SHIPIT_PYTHON_VERSION",
49
- default_version=python_version,
50
- use_in_build=True,
51
- use_in_serve=True,
52
- ),
53
- DependencySpec(
54
- "uv",
55
- env_var="SHIPIT_UV_VERSION",
56
- default_version="0.8.15",
57
- use_in_build=True,
58
- ),
59
- ]
60
-
61
- def declarations(self) -> Optional[str]:
62
- return (
63
- "cross_platform = getenv(\"SHIPIT_PYTHON_CROSS_PLATFORM\")\n"
64
- "python_extra_index_url = getenv(\"SHIPIT_PYTHON_EXTRA_INDEX_URL\")\n"
65
- "precompile_python = getenv(\"SHIPIT_PYTHON_PRECOMPILE\") in [\"true\", \"True\", \"TRUE\", \"1\", \"on\", \"yes\", \"y\", \"Y\", \"YES\", \"On\", \"ON\"]\n"
66
- "python_cross_packages_path = venv[\"build\"] + f\"/lib/python{python_version}/site-packages\""
67
- )
68
-
69
- def build_steps(self) -> list[str]:
70
- steps = [
71
- "workdir(app[\"build\"])"
72
- ]
73
-
74
- if _exists(self.path, "pyproject.toml"):
75
- input_files = ["pyproject.toml"]
76
- extra_args = ""
77
- if _exists(self.path, "uv.lock"):
78
- input_files.append("uv.lock")
79
- extra_args = " --locked"
80
- inputs = ", ".join([f"\"{input}\"" for input in input_files])
81
- steps += [
82
- "env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
83
- "run(f\"uv sync --compile --python python{python_version} --no-managed-python" + extra_args + "\", inputs=[" + inputs + "], group=\"install\")",
84
- "run(f\"uv pip compile pyproject.toml --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --only-binary :all: -o cross-requirements.txt\", inputs=[\"pyproject.toml\"], outputs=[\"cross-requirements.txt\"]) if cross_platform else None",
85
- "run(f\"uvx pip install -r cross-requirements.txt --target {python_cross_packages_path} --platform {cross_platform} --only-binary=:all: --python-version={python_version} --compile\") if cross_platform else None",
86
- "run(\"rm cross-requirements.txt\") if cross_platform else None",
87
- ]
88
- if _exists(self.path, "requirements.txt"):
89
- steps += [
90
- "env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
91
- "run(f\"uv init --no-managed-python --python python{python_version}\", inputs=[], outputs=[\"uv.lock\"], group=\"install\")",
92
- "run(f\"uv add -r requirements.txt\", inputs=[\"requirements.txt\"], group=\"install\")",
93
- "run(f\"uv pip compile requirements.txt --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --only-binary :all: -o cross-requirements.txt\", inputs=[\"requirements.txt\"], outputs=[\"cross-requirements.txt\"]) if cross_platform else None",
94
- "run(f\"uvx pip install -r cross-requirements.txt --target {python_cross_packages_path} --platform {cross_platform} --only-binary=:all: --python-version={python_version} --compile\") if cross_platform else None",
95
- "run(\"rm cross-requirements.txt\") if cross_platform else None",
96
- ]
97
-
98
- steps += [
99
- "path((local_venv[\"build\"] if cross_platform else venv[\"build\"]) + \"/bin\")",
100
- "copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
101
- ]
102
- return steps
103
-
104
- def prepare_steps(self) -> Optional[list[str]]:
105
- return [
106
- 'workdir(app["serve"])',
107
- 'run("echo \\\"Precompiling Python code...\\\"") if precompile_python else None',
108
- 'run("python -m compileall -o 2 $PYTHONPATH") if precompile_python else None',
109
- 'run("echo \\\"Precompiling package code...\\\"") if precompile_python else None',
110
- 'run("python -m compileall -o 2 .") if precompile_python else None',
111
- ]
112
-
113
- def commands(self) -> Dict[str, str]:
114
- if _exists(self.path, "manage.py"):
115
- start_cmd = '"python manage.py runserver 0.0.0.0:8000"'
116
- migrate_cmd = '"python manage.py migrate"'
117
- return {"start": start_cmd, "after_deploy": migrate_cmd}
118
- elif _exists(self.path, "main.py"):
119
- start_cmd = '"python main.py"'
120
- elif _exists(self.path, "src/main.py"):
121
- start_cmd = '"python src/main.py"'
122
- else:
123
- start_cmd = '"python -c \'print(\\\"Hello, World!\\\")\'"'
124
- return {"start": start_cmd}
125
-
126
- def assets(self) -> Optional[Dict[str, str]]:
127
- return None
128
-
129
- def mounts(self) -> list[MountSpec]:
130
- return [
131
- MountSpec("app"),
132
- MountSpec("venv"),
133
- MountSpec("local_venv", attach_to_serve=False),
134
- ]
135
-
136
- def env(self) -> Optional[Dict[str, str]]:
137
- # For Django projects, generate an empty env dict to surface the field
138
- # in the Shipit file. Other Python projects omit it by default.
139
- return {
140
- "PYTHONPATH": "\"{}/lib/python{}/site-packages\".format(venv[\"serve\"], python_version)"
141
- }
@@ -1,5 +0,0 @@
1
- __all__ = ["version", "version_info"]
2
-
3
-
4
- version = "0.3.4"
5
- version_info = (0, 3, 4, "final", 0)
File without changes
File without changes