shipit-cli 0.5.1__tar.gz → 0.5.2__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.5.1 → shipit_cli-0.5.2}/PKG-INFO +1 -1
  2. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/pyproject.toml +1 -1
  3. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/cli.py +22 -9
  4. shipit_cli-0.5.2/src/shipit/providers/mkdocs.py +60 -0
  5. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/providers/python.py +173 -90
  6. shipit_cli-0.5.2/src/shipit/version.py +5 -0
  7. shipit_cli-0.5.1/src/shipit/providers/mkdocs.py +0 -88
  8. shipit_cli-0.5.1/src/shipit/version.py +0 -5
  9. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/.gitignore +0 -0
  10. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/README.md +0 -0
  11. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/__init__.py +0 -0
  12. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/assets/php/php.ini +0 -0
  13. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/generator.py +0 -0
  14. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/providers/base.py +0 -0
  15. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/providers/gatsby.py +0 -0
  16. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/providers/hugo.py +0 -0
  17. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/providers/laravel.py +0 -0
  18. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/providers/node_static.py +0 -0
  19. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/providers/php.py +0 -0
  20. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/providers/registry.py +0 -0
  21. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/src/shipit/providers/staticfile.py +0 -0
  22. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/tests/test_examples_build.py +0 -0
  23. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/tests/test_generate_shipit_examples.py +0 -0
  24. {shipit_cli-0.5.1 → shipit_cli-0.5.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipit-cli
3
- Version: 0.5.1
3
+ Version: 0.5.2
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.5.1"
3
+ version = "0.5.2"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -594,10 +594,10 @@ class LocalBuilder:
594
594
  commands.append(step.command)
595
595
  elif isinstance(step, WorkdirStep):
596
596
  commands.append(f"cd {step.path}")
597
- content = "#!/bin/bash\n{body}".format(
598
- body="\n".join(commands)
597
+ content = "#!/bin/bash\n{body}".format(body="\n".join(commands))
598
+ console.print(
599
+ f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]"
599
600
  )
600
- console.print(f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]")
601
601
  manifest_panel = Panel(
602
602
  Syntax(
603
603
  content,
@@ -669,8 +669,8 @@ class WasmerBuilder:
669
669
  mapper: Dict[str, MapperItem] = {
670
670
  "python": {
671
671
  "dependencies": {
672
- "latest": "wasmer/python-native@=0.1.11",
673
- "3.13": "wasmer/python-native@=0.1.11",
672
+ "latest": "python/python@=3.13.1",
673
+ "3.13": "python/python@=3.13.1",
674
674
  },
675
675
  "scripts": {"python"},
676
676
  "aliases": {},
@@ -679,6 +679,13 @@ class WasmerBuilder:
679
679
  "PYTHONHOME": "/cpython",
680
680
  },
681
681
  },
682
+ "pandoc": {
683
+ "dependencies": {
684
+ "latest": "wasmer/pandoc@=0.0.1",
685
+ "3.5": "wasmer/pandoc@=0.0.1",
686
+ },
687
+ "scripts": {"pandoc"},
688
+ },
682
689
  "php": {
683
690
  "dependencies": {
684
691
  "latest": "php/php-32@=8.3.2104",
@@ -773,7 +780,9 @@ class WasmerBuilder:
773
780
 
774
781
  body = "\n".join(filter(None, [env_lines, *commands]))
775
782
  content = f"#!/bin/bash\n\n{body}"
776
- console.print(f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]")
783
+ console.print(
784
+ f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]"
785
+ )
777
786
  manifest_panel = Panel(
778
787
  Syntax(
779
788
  content,
@@ -840,12 +849,14 @@ class WasmerBuilder:
840
849
  version
841
850
  ].split("@")
842
851
  dependencies.add(package_name, version)
843
- for script in self.mapper[dep.name]["scripts"]:
852
+ scripts = self.mapper[dep.name].get("scripts") or []
853
+ for script in scripts:
844
854
  binaries[script] = {
845
855
  "script": f"{package_name}:{script}",
846
856
  "env": self.mapper[dep.name].get("env"),
847
857
  }
848
- for alias, script in self.mapper[dep.name]["aliases"].items():
858
+ aliases = self.mapper[dep.name].get("aliases") or {}
859
+ for alias, script in aliases.items():
849
860
  binaries[alias] = {
850
861
  "script": f"{package_name}:{script}",
851
862
  "env": self.mapper[dep.name].get("env"),
@@ -922,7 +933,9 @@ class WasmerBuilder:
922
933
 
923
934
  original_app_yaml_path = self.src_dir / "app.yaml"
924
935
  if original_app_yaml_path.exists():
925
- console.print(f"[bold]Using original app.yaml found in source directory[/bold]")
936
+ console.print(
937
+ f"[bold]Using original app.yaml found in source directory[/bold]"
938
+ )
926
939
  yaml_config = yaml.safe_load(original_app_yaml_path.read_text())
927
940
  else:
928
941
  yaml_config = {
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
7
+ from .staticfile import StaticFileProvider
8
+ from .python import PythonProvider
9
+
10
+
11
+ class MkdocsProvider(StaticFileProvider):
12
+ def __init__(self, path: Path):
13
+ self.path = path
14
+ self.python_provider = PythonProvider(path, only_build=True, extra_dependencies={"mkdocs"})
15
+
16
+ @classmethod
17
+ def name(cls) -> str:
18
+ return "mkdocs"
19
+
20
+ @classmethod
21
+ def detect(cls, path: Path) -> Optional[DetectResult]:
22
+ if _exists(path, "mkdocs.yml", "mkdocs.yaml"):
23
+ return DetectResult(cls.name(), 85)
24
+ return None
25
+
26
+ def initialize(self) -> None:
27
+ pass
28
+
29
+ def serve_name(self) -> str:
30
+ return self.path.name
31
+
32
+ def provider_kind(self) -> str:
33
+ return "mkdocs-site"
34
+
35
+ def dependencies(self) -> list[DependencySpec]:
36
+ return [
37
+ *self.python_provider.dependencies(),
38
+ *super().dependencies(),
39
+ ]
40
+
41
+ def declarations(self) -> Optional[str]:
42
+ return "mkdocs_version = getenv(\"SHIPIT_MKDOCS_VERSION\") or \"1.6.1\"\n" + (self.python_provider.declarations() or "")
43
+
44
+ def build_steps(self) -> list[str]:
45
+ return [
46
+ *self.python_provider.build_steps(),
47
+ "run(\"uv run mkdocs build --site-dir={}\".format(app[\"build\"]), outputs=[\".\"], group=\"build\")",
48
+ ]
49
+
50
+ def prepare_steps(self) -> Optional[list[str]]:
51
+ return self.python_provider.prepare_steps()
52
+
53
+ def assets(self) -> Optional[Dict[str, str]]:
54
+ return None
55
+
56
+ def mounts(self) -> list[MountSpec]:
57
+ return [MountSpec("app"), *self.python_provider.mounts()]
58
+
59
+ def env(self) -> Optional[Dict[str, str]]:
60
+ return self.python_provider.env()
@@ -29,6 +29,7 @@ class PythonServer(Enum):
29
29
  # Gunicorn = "gunicorn"
30
30
  Daphne = "daphne"
31
31
 
32
+
32
33
  class DatabaseType(Enum):
33
34
  MySQL = "mysql"
34
35
  PostgreSQL = "postgresql"
@@ -41,8 +42,17 @@ class PythonProvider:
41
42
  extra_dependencies: Set[str]
42
43
  asgi_application: Optional[str] = None
43
44
  wsgi_application: Optional[str] = None
44
-
45
- def __init__(self, path: Path):
45
+ uses_ffmpeg: bool = False
46
+ uses_pandoc: bool = False
47
+ only_build: bool = False
48
+ install_requires_all_files: bool = False
49
+
50
+ def __init__(
51
+ self,
52
+ path: Path,
53
+ only_build: bool = False,
54
+ extra_dependencies: Optional[Set[str]] = None,
55
+ ):
46
56
  self.path = path
47
57
  if _exists(self.path, ".python-version"):
48
58
  python_version = (self.path / ".python-version").read_text().strip()
@@ -50,6 +60,11 @@ class PythonProvider:
50
60
  python_version = "3.13"
51
61
  self.default_python_version = python_version
52
62
  self.extra_dependencies = set()
63
+ self.only_build = only_build
64
+ self.extra_dependencies = extra_dependencies or set()
65
+
66
+ if self.only_build:
67
+ return
53
68
 
54
69
  pg_deps = {
55
70
  "asyncpg",
@@ -57,9 +72,11 @@ class PythonProvider:
57
72
  "psycopg",
58
73
  "psycopg2",
59
74
  "psycopg-binary",
60
- "psycopg2-binary"}
75
+ "psycopg2-binary",
76
+ }
61
77
  mysql_deps = {"mysqlclient", "pymysql", "mysql-connector-python", "aiomysql"}
62
78
  found_deps = self.check_deps(
79
+ "file://", # This is not really a dependency, but as a way to check if the install script requires all files
63
80
  "streamlit",
64
81
  "django",
65
82
  "mcp",
@@ -69,11 +86,17 @@ class PythonProvider:
69
86
  "daphne",
70
87
  "hypercorn",
71
88
  "uvicorn",
89
+ # Other
90
+ "ffmpeg",
91
+ "pandoc",
72
92
  # "gunicorn",
73
93
  *mysql_deps,
74
94
  *pg_deps,
75
95
  )
76
96
 
97
+ if "file://" in found_deps:
98
+ self.install_requires_all_files = True
99
+
77
100
  # ASGI/WSGI Server
78
101
  if "uvicorn" in found_deps:
79
102
  server = PythonServer.Uvicorn
@@ -87,17 +110,30 @@ class PythonProvider:
87
110
  server = None
88
111
  self.server = server
89
112
 
113
+ if "ffmpeg" in found_deps:
114
+ self.uses_ffmpeg = True
115
+ if "pandoc" in found_deps:
116
+ self.uses_pandoc = True
117
+
90
118
  # Set framework
91
119
  if _exists(self.path, "manage.py") and ("django" in found_deps):
92
120
  framework = PythonFramework.Django
93
121
  # Find the settings.py file using glob
94
- settings_file = next(self.path.glob( "**/settings.py"))
122
+ try:
123
+ settings_file = next(self.path.glob("**/settings.py"))
124
+ except StopIteration:
125
+ settings_file = None
95
126
  if settings_file:
96
- asgi_match = re.search(r"ASGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text())
127
+ asgi_match = re.search(
128
+ r"ASGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text()
129
+ )
97
130
  if asgi_match:
98
131
  self.asgi_application = asgi_match.group(1)
99
132
  else:
100
- wsgi_match = re.search(r"WSGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text())
133
+ wsgi_match = re.search(
134
+ r"WSGI_APPLICATION\s*=\s*['\"](.*)['\"]",
135
+ settings_file.read_text(),
136
+ )
101
137
  if wsgi_match:
102
138
  self.wsgi_application = wsgi_match.group(1)
103
139
 
@@ -154,7 +190,7 @@ class PythonProvider:
154
190
  break
155
191
  if not deps:
156
192
  break
157
- return initial_deps-deps
193
+ return initial_deps - deps
158
194
 
159
195
  @classmethod
160
196
  def name(cls) -> str:
@@ -178,7 +214,7 @@ class PythonProvider:
178
214
  return "python"
179
215
 
180
216
  def dependencies(self) -> list[DependencySpec]:
181
- return [
217
+ deps = [
182
218
  DependencySpec(
183
219
  "python",
184
220
  env_var="SHIPIT_PYTHON_VERSION",
@@ -193,73 +229,131 @@ class PythonProvider:
193
229
  use_in_build=True,
194
230
  ),
195
231
  ]
232
+ if self.uses_pandoc:
233
+ deps.append(
234
+ DependencySpec(
235
+ "pandoc",
236
+ env_var="SHIPIT_PANDOC_VERSION",
237
+ use_in_build=False,
238
+ use_in_serve=True,
239
+ )
240
+ )
241
+ return deps
196
242
 
197
243
  def declarations(self) -> Optional[str]:
244
+ if self.only_build:
245
+ return (
246
+ 'cross_platform = getenv("SHIPIT_PYTHON_CROSS_PLATFORM")\n'
247
+ "venv = local_venv\n"
248
+ )
198
249
  return (
199
- "cross_platform = getenv(\"SHIPIT_PYTHON_CROSS_PLATFORM\")\n"
200
- "python_extra_index_url = getenv(\"SHIPIT_PYTHON_EXTRA_INDEX_URL\")\n"
201
- "precompile_python = getenv(\"SHIPIT_PYTHON_PRECOMPILE\") in [\"true\", \"True\", \"TRUE\", \"1\", \"on\", \"yes\", \"y\", \"Y\", \"YES\", \"On\", \"ON\"]\n"
202
- "python_cross_packages_path = venv[\"build\"] + f\"/lib/python{python_version}/site-packages\"\n"
203
- "python_serve_path = \"{}/lib/python{}/site-packages\".format(venv[\"serve\"], python_version)\n"
250
+ 'cross_platform = getenv("SHIPIT_PYTHON_CROSS_PLATFORM")\n'
251
+ 'python_extra_index_url = getenv("SHIPIT_PYTHON_EXTRA_INDEX_URL")\n'
252
+ 'precompile_python = getenv("SHIPIT_PYTHON_PRECOMPILE") in ["true", "True", "TRUE", "1", "on", "yes", "y", "Y", "YES", "On", "ON"]\n'
253
+ 'python_cross_packages_path = venv["build"] + f"/lib/python{python_version}/site-packages"\n'
254
+ 'python_serve_path = "{}/lib/python{}/site-packages".format(venv["serve"], python_version)\n'
204
255
  )
205
256
 
206
257
  def build_steps(self) -> list[str]:
207
- steps = [
208
- "workdir(app[\"build\"])"
209
- ]
258
+ if not self.only_build:
259
+ steps = ['workdir(app["build"])']
260
+ else:
261
+ steps = []
210
262
 
211
263
  extra_deps = ", ".join([f"{dep}" for dep in self.extra_dependencies])
264
+ has_requirements = _exists(self.path, "requirements.txt")
212
265
  if _exists(self.path, "pyproject.toml"):
213
266
  input_files = ["pyproject.toml"]
214
267
  extra_args = ""
215
268
  if _exists(self.path, "uv.lock"):
216
269
  input_files.append("uv.lock")
217
270
  extra_args = " --locked"
218
- inputs = ", ".join([f"\"{input}\"" for input in input_files])
271
+ inputs = ", ".join([f'"{input}"' for input in input_files])
219
272
  steps += [
220
- "env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
221
- f"run(f\"uv sync --compile --python python{{python_version}} --no-managed-python{extra_args}\", inputs=[{inputs}], group=\"install\")",
222
- "copy(\"pyproject.toml\", \"pyproject.toml\")",
223
- f"run(\"uv add {extra_deps}\", group=\"install\")" if extra_deps else None,
224
- "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",
225
- 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",
226
- "run(\"rm cross-requirements.txt\") if cross_platform else None",
273
+ 'env(UV_PROJECT_ENVIRONMENT=local_venv["build"] if cross_platform else venv["build"])',
274
+ 'copy(".", ".")' if self.install_requires_all_files else None,
275
+ f'run(f"uv sync --compile --python python{{python_version}} --no-managed-python{extra_args}", inputs=[{inputs}], group="install")',
276
+ 'copy("pyproject.toml", "pyproject.toml")'
277
+ if not self.install_requires_all_files
278
+ else None,
279
+ f'run("uv add {extra_deps}", group="install")' if extra_deps else None,
227
280
  ]
228
- if _exists(self.path, "requirements.txt"):
281
+ if not self.only_build:
282
+ steps += [
283
+ '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',
284
+ 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',
285
+ 'run("rm cross-requirements.txt") if cross_platform else None',
286
+ ]
287
+ elif has_requirements or extra_deps:
229
288
  steps += [
230
- "env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
231
- "run(f\"uv init --no-workspace --no-managed-python --python python{python_version}\", inputs=[], outputs=[\"uv.lock\"], group=\"install\")",
232
- f"run(\"uv add -r requirements.txt {extra_deps}\", inputs=[\"requirements.txt\"], group=\"install\")",
233
- "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",
234
- 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",
235
- "run(\"rm cross-requirements.txt\") if cross_platform else None",
289
+ 'env(UV_PROJECT_ENVIRONMENT=local_venv["build"] if cross_platform else venv["build"])',
290
+ 'run(f"uv init --no-workspace --no-managed-python --python python{python_version}", inputs=[], outputs=["uv.lock"], group="install")',
291
+ 'copy(".", ".")' if self.install_requires_all_files else None,
236
292
  ]
293
+ if has_requirements:
294
+ steps += [
295
+ f'run("uv add -r requirements.txt {extra_deps}", inputs=["requirements.txt"], group="install")',
296
+ ]
297
+ else:
298
+ steps += [
299
+ f'run("uv add {extra_deps}", group="install")',
300
+ ]
301
+ if not self.only_build:
302
+ steps += [
303
+ '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',
304
+ 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',
305
+ 'run("rm cross-requirements.txt") if cross_platform else None',
306
+ ]
237
307
 
238
308
  steps += [
239
- "path((local_venv[\"build\"] if cross_platform else venv[\"build\"]) + \"/bin\")",
240
- "copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
309
+ 'path((local_venv["build"] if cross_platform else venv["build"]) + "/bin")',
310
+ 'copy(".", ".", ignore=[".venv", ".git", "__pycache__"])',
241
311
  ]
242
312
  if self.framework == PythonFramework.MCP:
243
313
  steps += [
244
- "run(\"mkdir -p {}/bin\".format(venv[\"build\"])) if cross_platform else None",
245
- "run(\"cp {}/bin/mcp {}/bin/mcp\".format(local_venv[\"build\"], venv[\"build\"])) if cross_platform else None",
314
+ 'run("mkdir -p {}/bin".format(venv["build"])) if cross_platform else None',
315
+ 'run("cp {}/bin/mcp {}/bin/mcp".format(local_venv["build"], venv["build"])) if cross_platform else None',
246
316
  ]
247
317
  return list(filter(None, steps))
248
318
 
249
319
  def prepare_steps(self) -> Optional[list[str]]:
320
+ if self.only_build:
321
+ return []
250
322
  return [
251
- 'run("echo \\\"Precompiling Python code...\\\"") if precompile_python else None',
323
+ 'run("echo \\"Precompiling Python code...\\"") if precompile_python else None',
252
324
  'run(f"python -m compileall -o 2 {python_serve_path}") if precompile_python else None',
253
- 'run("echo \\\"Precompiling package code...\\\"") if precompile_python else None',
325
+ 'run("echo \\"Precompiling package code...\\"") if precompile_python else None',
254
326
  'run("python -m compileall -o 2 {}".format(app["serve"])) if precompile_python else None',
255
327
  ]
256
328
 
329
+ def get_main_file(self) -> Optional[str]:
330
+ paths_to_try = ["main.py", "app.py", "streamlit_app.py", "Home.py", "*_app.py"]
331
+ for path in paths_to_try:
332
+ if "*" in path:
333
+ continue # This is for the glob finder
334
+ if _exists(self.path, path):
335
+ return path
336
+ if _exists(self.path, f"src/{path}"):
337
+ return f"src/{path}"
338
+ for path in paths_to_try:
339
+ try:
340
+ found_path = next(self.path.glob(f"**/{path}.py"))
341
+ except StopIteration:
342
+ found_path = None
343
+ if found_path:
344
+ return found_path.relative_to(self.path)
345
+ return None
346
+
257
347
  def commands(self) -> Dict[str, str]:
348
+ if self.only_build:
349
+ return {}
258
350
  if self.framework == PythonFramework.Django:
259
351
  start_cmd = None
260
352
  if self.server == PythonServer.Daphne and self.asgi_application:
261
353
  asgi_application = format_app_import(self.asgi_application)
262
- start_cmd = f'"python -m daphne {asgi_application} --bind 0.0.0.0 --port 8000"'
354
+ start_cmd = (
355
+ f'"python -m daphne {asgi_application} --bind 0.0.0.0 --port 8000"'
356
+ )
263
357
  elif self.server == PythonServer.Uvicorn:
264
358
  if self.asgi_application:
265
359
  asgi_application = format_app_import(self.asgi_application)
@@ -274,73 +368,58 @@ class PythonProvider:
274
368
  start_cmd = '"python manage.py runserver 0.0.0.0:8000"'
275
369
  migrate_cmd = '"python manage.py migrate"'
276
370
  return {"start": start_cmd, "after_deploy": migrate_cmd}
277
- elif self.framework == PythonFramework.FastAPI:
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
-
371
+
372
+ main_file = self.get_main_file()
373
+
374
+ if not main_file:
375
+ start_cmd = '"python -c \'print(\\"No start command detected, please provide a start command manually\\")\'"'
376
+ return {"start": start_cmd}
377
+
378
+ if self.framework == PythonFramework.FastAPI:
379
+ python_path = file_to_python_path(main_file)
380
+ path = f"{python_path}:app"
283
381
  if self.server == PythonServer.Uvicorn:
284
382
  start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
285
383
  elif self.server == PythonServer.Hypercorn:
286
384
  start_cmd = f'"python -m hypercorn {path} --bind 0.0.0.0:8000"'
287
385
  else:
288
- start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
386
+ start_cmd = '"python -c \'print(\\"No start command detected, please provide a start command manually\\")\'"'
289
387
  return {"start": start_cmd}
388
+
290
389
  elif self.framework == PythonFramework.Streamlit:
291
- if _exists(self.path, "main.py"):
292
- path = "main.py"
293
- elif _exists(self.path, "src/main.py"):
294
- path = "src/main.py"
295
- if _exists(self.path, "app.py"):
296
- path = "app.py"
297
- elif _exists(self.path, "src/app.py"):
298
- path = "src/app.py"
299
- elif _exists(self.path, "src/streamlit_app.py"):
300
- path = "src/streamlit_app.py"
301
- elif _exists(self.path, "streamlit_app.py"):
302
- path = "streamlit_app.py"
303
- elif _exists(self.path, "Home.py"):
304
- path = "Home.py"
305
-
306
- start_cmd = f'"python -m streamlit run {path} --server.port 8000 --server.address 0.0.0.0 --server.headless true"'
390
+ start_cmd = f'"python -m streamlit run {main_file} --server.port 8000 --server.address 0.0.0.0 --server.headless true"'
391
+
307
392
  elif self.framework == PythonFramework.Flask:
308
- if _exists(self.path, "main.py"):
309
- path = "main:app"
310
- if _exists(self.path, "app.py"):
311
- path = "app:app"
312
- elif _exists(self.path, "src/main.py"):
313
- path = "src.main:app"
393
+ python_path = file_to_python_path(main_file)
394
+ path = f"{python_path}:app"
314
395
  # start_cmd = f'"python -m flask --app {path} run --debug --host 0.0.0.0 --port 8000"'
315
396
  start_cmd = f'"python -m uvicorn {path} --interface=wsgi --host 0.0.0.0 --port 8000"'
397
+
316
398
  elif self.framework == PythonFramework.MCP:
317
- if _exists(self.path, "main.py"):
318
- path = "main.py"
399
+ contents = (self.path / main_file).read_text()
400
+ if 'if __name__ == "__main__"' in contents or "mcp.run" in contents:
401
+ start_cmd = f'"python {main_file}"'
319
402
  else:
320
- path = next(self.path.glob( "**/main.py"))
321
- if path:
322
- path = path.relative_to(self.path)
323
- else:
324
- path = "main.py"
325
- start_cmd = f'"python {{}}/bin/mcp run {path} --transport=streamable-http".format(venv[\"serve\"])'
403
+ start_cmd = f'"python {{}}/bin/mcp run {main_file} --transport=streamable-http".format(venv["serve"])'
404
+
326
405
  elif self.framework == PythonFramework.FastHTML:
327
- if _exists(self.path, "main.py"):
328
- path = "main:app"
329
- elif _exists(self.path, "src/main.py"):
330
- path = "src.main:app"
406
+ python_path = file_to_python_path(main_file)
407
+ path = f"{python_path}:app"
331
408
  start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
332
- elif _exists(self.path, "main.py"):
333
- start_cmd = '"python main.py"'
334
- elif _exists(self.path, "src/main.py"):
335
- start_cmd = '"python src/main.py"'
409
+
336
410
  else:
337
- start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
411
+ start_cmd = f'"python {main_file}"'
412
+
338
413
  return {"start": start_cmd}
339
414
 
340
415
  def assets(self) -> Optional[Dict[str, str]]:
341
416
  return None
342
417
 
343
418
  def mounts(self) -> list[MountSpec]:
419
+ if self.only_build:
420
+ return [
421
+ MountSpec("local_venv", attach_to_serve=False),
422
+ ]
344
423
  return [
345
424
  MountSpec("app"),
346
425
  MountSpec("venv"),
@@ -348,19 +427,23 @@ class PythonProvider:
348
427
  ]
349
428
 
350
429
  def env(self) -> Optional[Dict[str, str]]:
430
+ if self.only_build:
431
+ return {}
351
432
  # For Django projects, generate an empty env dict to surface the field
352
433
  # in the Shipit file. Other Python projects omit it by default.
353
- env_vars = {
354
- "PYTHONPATH": "python_serve_path",
355
- "HOME": "app[\"serve\"]"
356
- }
434
+ env_vars = {"PYTHONPATH": "python_serve_path", "HOME": 'app["serve"]'}
357
435
  if self.framework == PythonFramework.Streamlit:
358
436
  env_vars["STREAMLIT_SERVER_HEADLESS"] = '"true"'
359
437
  elif self.framework == PythonFramework.MCP:
360
- env_vars["FASTMCP_HOST"] = "\"0.0.0.0\""
361
- env_vars["FASTMCP_PORT"] = "\"8000\""
438
+ env_vars["FASTMCP_HOST"] = '"0.0.0.0"'
439
+ env_vars["FASTMCP_PORT"] = '"8000"'
362
440
  return env_vars
363
441
 
442
+
364
443
  def format_app_import(asgi_application: str) -> str:
365
444
  # Transform "mysite.asgi.application" to "mysite.asgi:application" using regex
366
445
  return re.sub(r"\.([^.]+)$", r":\1", asgi_application)
446
+
447
+
448
+ def file_to_python_path(path: str) -> str:
449
+ return path.rstrip(".py").replace("/", ".").replace("\\", ".")
@@ -0,0 +1,5 @@
1
+ __all__ = ["version", "version_info"]
2
+
3
+
4
+ version = "0.5.2"
5
+ version_info = (0, 5, 2, "final", 0)
@@ -1,88 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from typing import Dict, Optional
5
-
6
- from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
7
-
8
-
9
- class MkdocsProvider:
10
- def __init__(self, path: Path):
11
- self.path = path
12
- @classmethod
13
- def name(cls) -> str:
14
- return "mkdocs"
15
-
16
- @classmethod
17
- def detect(cls, path: Path) -> Optional[DetectResult]:
18
- if _exists(path, "mkdocs.yml", "mkdocs.yaml"):
19
- return DetectResult(cls.name(), 85)
20
- return None
21
-
22
- def initialize(self) -> None:
23
- pass
24
-
25
- def serve_name(self) -> str:
26
- return self.path.name
27
-
28
- def provider_kind(self) -> str:
29
- return "mkdocs-site"
30
-
31
- def dependencies(self) -> list[DependencySpec]:
32
- return [
33
- DependencySpec(
34
- "python",
35
- env_var="SHIPIT_PYTHON_VERSION",
36
- default_version="3.13",
37
- use_in_build=True,
38
- ),
39
- DependencySpec(
40
- "uv",
41
- env_var="SHIPIT_UV_VERSION",
42
- default_version="0.8.15",
43
- use_in_build=True,
44
- ),
45
- DependencySpec(
46
- "static-web-server",
47
- env_var="SHIPIT_SWS_VERSION",
48
- default_version="2.38.0",
49
- use_in_serve=True,
50
- ),
51
- ]
52
-
53
- def declarations(self) -> Optional[str]:
54
- return None
55
-
56
- def build_steps(self) -> list[str]:
57
- has_requirements = _exists(self.path, "requirements.txt")
58
- if has_requirements:
59
- install_lines = [
60
- "run(\"uv init --no-managed-python\", inputs=[], outputs=[\".\"], group=\"install\")",
61
- "run(f\"uv add -r requirements.txt\", inputs=[\"requirements.txt\"], outputs=[\".venv\"], group=\"install\")",
62
- ]
63
- else:
64
- install_lines = [
65
- "mkdocs_version = getenv(\"SHIPIT_MKDOCS_VERSION\") or \"1.6.1\"",
66
- "run(\"uv init --no-managed-python\", inputs=[], outputs=[\".venv\"], group=\"install\")",
67
- "run(f\"uv add mkdocs=={mkdocs_version}\", group=\"install\")",
68
- ]
69
- return [
70
- *install_lines,
71
- "copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
72
- "run(\"uv run mkdocs build --site-dir={}\".format(app[\"build\"]), outputs=[\".\"], group=\"build\")",
73
- ]
74
-
75
- def prepare_steps(self) -> Optional[list[str]]:
76
- return None
77
-
78
- def commands(self) -> Dict[str, str]:
79
- return {"start": '"static-web-server --root /app"'}
80
-
81
- def assets(self) -> Optional[Dict[str, str]]:
82
- return None
83
-
84
- def mounts(self) -> list[MountSpec]:
85
- return [MountSpec("app")]
86
-
87
- def env(self) -> Optional[Dict[str, str]]:
88
- return None
@@ -1,5 +0,0 @@
1
- __all__ = ["version", "version_info"]
2
-
3
-
4
- version = "0.5.1"
5
- version_info = (0, 5, 1, "final", 0)
File without changes
File without changes