shipit-cli 0.3.3__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/PKG-INFO +1 -1
  2. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/pyproject.toml +1 -1
  3. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/cli.py +45 -6
  4. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/generator.py +24 -22
  5. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/base.py +17 -14
  6. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/gatsby.py +19 -15
  7. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/hugo.py +12 -10
  8. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/laravel.py +19 -15
  9. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/mkdocs.py +20 -16
  10. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/node_static.py +21 -17
  11. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/php.py +26 -22
  12. shipit_cli-0.4.0/src/shipit/providers/python.py +309 -0
  13. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/registry.py +9 -10
  14. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/staticfile.py +21 -16
  15. shipit_cli-0.4.0/src/shipit/version.py +5 -0
  16. shipit_cli-0.3.3/src/shipit/providers/python.py +0 -135
  17. shipit_cli-0.3.3/src/shipit/version.py +0 -5
  18. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/.gitignore +0 -0
  19. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/README.md +0 -0
  20. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/__init__.py +0 -0
  21. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/assets/php/php.ini +0 -0
  22. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/tests/test_examples_build.py +0 -0
  23. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/tests/test_generate_shipit_examples.py +0 -0
  24. {shipit_cli-0.3.3 → shipit_cli-0.4.0}/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.3
3
+ Version: 0.4.0
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.3"
3
+ version = "0.4.0"
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__":
@@ -7,22 +7,19 @@ from shipit.providers.base import DependencySpec, Provider, ProviderPlan, Detect
7
7
  from shipit.providers.registry import providers as registry_providers
8
8
 
9
9
 
10
- def _providers() -> list[Provider]:
11
- # Load providers from modular registry
10
+ def _providers() -> list[type[Provider]]:
11
+ # Load provider classes from modular registry
12
12
  return registry_providers()
13
13
 
14
14
 
15
15
  def detect_provider(path: Path) -> Provider:
16
- matches: list[tuple[Provider, DetectResult]] = []
17
- for p in _providers():
18
- res = p.detect(path)
16
+ matches: list[tuple[type[Provider], DetectResult]] = []
17
+ for provider_cls in _providers():
18
+ res = provider_cls.detect(path)
19
19
  if res:
20
- matches.append((p, res))
20
+ matches.append((provider_cls, res))
21
21
  if not matches:
22
- # Default to static site as the safest fallback
23
- from shipit.providers.staticfile import StaticFileProvider
24
-
25
- return StaticFileProvider()
22
+ raise Exception("Shipit could not detect a provider for this project")
26
23
  # Highest score wins; tie-breaker by order
27
24
  matches.sort(key=lambda x: x[1].score, reverse=True)
28
25
  return matches[0][0]
@@ -80,20 +77,20 @@ def _render_assets(assets: Optional[Dict[str, str]]) -> Optional[str]:
80
77
 
81
78
 
82
79
  def generate_shipit(path: Path) -> str:
83
- provider = detect_provider(path)
84
- provider.initialize(path)
80
+ provider_cls = detect_provider(path)
81
+ provider = provider_cls(path)
85
82
 
86
83
  # Collect parts
87
84
  plan = ProviderPlan(
88
- serve_name=provider.serve_name(path),
89
- provider=provider.provider_kind(path),
90
- mounts=provider.mounts(path),
91
- declarations=provider.declarations(path),
92
- dependencies=provider.dependencies(path),
93
- build_steps=provider.build_steps(path),
94
- prepare=provider.prepare_steps(path),
95
- commands=provider.commands(path),
96
- env=provider.env(path),
85
+ serve_name=provider.serve_name(),
86
+ provider=provider.provider_kind(),
87
+ mounts=provider.mounts(),
88
+ declarations=provider.declarations(),
89
+ dependencies=provider.dependencies(),
90
+ build_steps=provider.build_steps(),
91
+ prepare=provider.prepare_steps(),
92
+ commands=provider.commands(),
93
+ env=provider.env(),
97
94
  )
98
95
 
99
96
  # Declare dependency variables (combined) and collect serve deps
@@ -118,8 +115,10 @@ def generate_shipit(path: Path) -> str:
118
115
  env_lines = ",\n".join([f' "{k}": {v}' for k, v in plan.env.items()])
119
116
  assets_block = _render_assets(plan.assets)
120
117
  mounts_block = None
118
+ attach_serve_names: list[str] = []
121
119
  if plan.mounts:
122
- 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]
123
122
  mounts_block = ",\n".join([f" {m.name}" for m in mounts])
124
123
 
125
124
  out: List[str] = []
@@ -134,6 +133,9 @@ def generate_shipit(path: Path) -> str:
134
133
  out.append("serve(")
135
134
  out.append(f' name="{plan.serve_name}",')
136
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"],')
137
139
  out.append(" build=[")
138
140
  out.append(build_steps_block)
139
141
  out.append(" ],")
@@ -12,21 +12,24 @@ class DetectResult:
12
12
 
13
13
 
14
14
  class Provider(Protocol):
15
- def name(self) -> str: ...
16
- def detect(self, path: Path) -> Optional[DetectResult]: ...
17
- def initialize(self, path: Path) -> None: ...
18
- # Structured plan steps
19
- def serve_name(self, path: Path) -> str: ...
20
- def provider_kind(self, path: Path) -> str: ...
21
- def dependencies(self, path: Path) -> list["DependencySpec"]: ...
22
- def declarations(self, path: Path) -> Optional[str]: ...
23
- def build_steps(self, path: Path) -> list[str]: ...
15
+ def __init__(self, path: Path): ...
16
+ @classmethod
17
+ def name(cls) -> str: ...
18
+ @classmethod
19
+ def detect(cls, path: Path) -> Optional[DetectResult]: ...
20
+ def initialize(self) -> None: ...
21
+ # Structured plan steps (no path args; use self.path)
22
+ def serve_name(self) -> str: ...
23
+ def provider_kind(self) -> str: ...
24
+ def dependencies(self) -> list["DependencySpec"]: ...
25
+ def declarations(self) -> Optional[str]: ...
26
+ def build_steps(self) -> list[str]: ...
24
27
  # Prepare: list of Starlark step calls (currently only run(...))
25
- def prepare_steps(self, path: Path) -> Optional[List[str]]: ...
26
- def commands(self, path: Path) -> Dict[str, str]: ...
27
- def assets(self, path: Path) -> Optional[Dict[str, str]]: ...
28
- def mounts(self, path: Path) -> List["MountSpec"]: ...
29
- def env(self, path: Path) -> Optional[Dict[str, str]]: ...
28
+ def prepare_steps(self) -> Optional[List[str]]: ...
29
+ def commands(self) -> Dict[str, str]: ...
30
+ def assets(self) -> Optional[Dict[str, str]]: ...
31
+ def mounts(self) -> List["MountSpec"]: ...
32
+ def env(self) -> Optional[Dict[str, str]]: ...
30
33
 
31
34
 
32
35
  @dataclass
@@ -7,32 +7,36 @@ from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependen
7
7
 
8
8
 
9
9
  class GatsbyProvider:
10
- def name(self) -> str:
10
+ def __init__(self, path: Path):
11
+ self.path = path
12
+ @classmethod
13
+ def name(cls) -> str:
11
14
  return "gatsby"
12
15
 
13
- def detect(self, path: Path) -> Optional[DetectResult]:
16
+ @classmethod
17
+ def detect(cls, path: Path) -> Optional[DetectResult]:
14
18
  pkg = path / "package.json"
15
19
  if not pkg.exists():
16
20
  return None
17
21
  if _exists(path, "gatsby-config.js", "gatsby-config.ts") or _has_dependency(
18
22
  pkg, "gatsby"
19
23
  ):
20
- return DetectResult(self.name(), 90)
24
+ return DetectResult(cls.name(), 90)
21
25
  return None
22
26
 
23
- def initialize(self, path: Path) -> None:
27
+ def initialize(self) -> None:
24
28
  pass
25
29
 
26
- def serve_name(self, path: Path) -> str:
27
- return path.name
30
+ def serve_name(self) -> str:
31
+ return self.path.name
28
32
 
29
- def provider_kind(self, path: Path) -> str:
33
+ def provider_kind(self) -> str:
30
34
  return "staticsite"
31
35
 
32
- def declarations(self, path: Path) -> Optional[str]:
36
+ def declarations(self) -> Optional[str]:
33
37
  return None
34
38
 
35
- def dependencies(self, path: Path) -> list[DependencySpec]:
39
+ def dependencies(self) -> list[DependencySpec]:
36
40
  return [
37
41
  DependencySpec(
38
42
  "node",
@@ -44,7 +48,7 @@ class GatsbyProvider:
44
48
  DependencySpec("static-web-server", env_var="SHIPIT_SWS_VERSION", use_in_serve=True),
45
49
  ]
46
50
 
47
- def build_steps(self, path: Path) -> list[str]:
51
+ def build_steps(self) -> list[str]:
48
52
  return [
49
53
  "run(\"npm install\", inputs=[\"package.json\", \"package-lock.json\"], group=\"install\")",
50
54
  "copy(\".\", \".\", ignore=[\"node_modules\", \".git\"])",
@@ -52,17 +56,17 @@ class GatsbyProvider:
52
56
  "run(\"cp -R public/* {}/\".format(app[\"build\"]))",
53
57
  ]
54
58
 
55
- def prepare_steps(self, path: Path) -> Optional[list[str]]:
59
+ def prepare_steps(self) -> Optional[list[str]]:
56
60
  return None
57
61
 
58
- def commands(self, path: Path) -> Dict[str, str]:
62
+ def commands(self) -> Dict[str, str]:
59
63
  return {"start": '"static-web-server --root /app"'}
60
64
 
61
- def assets(self, path: Path) -> Optional[Dict[str, str]]:
65
+ def assets(self) -> Optional[Dict[str, str]]:
62
66
  return None
63
67
 
64
- def mounts(self, path: Path) -> list[MountSpec]:
68
+ def mounts(self) -> list[MountSpec]:
65
69
  return [MountSpec("app")]
66
70
 
67
- def env(self, path: Path) -> Optional[Dict[str, str]]:
71
+ def env(self) -> Optional[Dict[str, str]]:
68
72
  return None
@@ -7,27 +7,29 @@ from .base import DetectResult, DependencySpec, Provider, _exists
7
7
  from .staticfile import StaticFileProvider
8
8
 
9
9
  class HugoProvider(StaticFileProvider):
10
- def name(self) -> str:
10
+ @classmethod
11
+ def name(cls) -> str:
11
12
  return "hugo"
12
13
 
13
- def detect(self, path: Path) -> Optional[DetectResult]:
14
+ @classmethod
15
+ def detect(cls, path: Path) -> Optional[DetectResult]:
14
16
  if _exists(path, "hugo.toml", "hugo.json", "hugo.yaml", "hugo.yml"):
15
- return DetectResult(self.name(), 80)
17
+ return DetectResult(cls.name(), 80)
16
18
  if (
17
19
  _exists(path, "config.toml", "config.json", "config.yaml", "config.yml")
18
20
  and _exists(path, "content")
19
21
  and (_exists(path, "static") or _exists(path, "themes"))
20
22
  ):
21
- return DetectResult(self.name(), 40)
23
+ return DetectResult(cls.name(), 40)
22
24
  return None
23
25
 
24
- def serve_name(self, path: Path) -> str:
25
- return path.name
26
+ def serve_name(self) -> str:
27
+ return self.path.name
26
28
 
27
- def provider_kind(self, path: Path) -> str:
29
+ def provider_kind(self) -> str:
28
30
  return "staticsite"
29
31
 
30
- def dependencies(self, path: Path) -> list[DependencySpec]:
32
+ def dependencies(self) -> list[DependencySpec]:
31
33
  return [
32
34
  DependencySpec(
33
35
  "hugo",
@@ -35,10 +37,10 @@ class HugoProvider(StaticFileProvider):
35
37
  default_version="0.149.0",
36
38
  use_in_build=True,
37
39
  ),
38
- *super().dependencies(path),
40
+ *super().dependencies(),
39
41
  ]
40
42
 
41
- def build_steps(self, path: Path) -> list[str]:
43
+ def build_steps(self) -> list[str]:
42
44
  return [
43
45
  'copy(".", ".", ignore=[".git"])',
44
46
  'run("hugo build --destination={}".format(app["build"]), group="build")',
@@ -7,24 +7,28 @@ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
7
7
 
8
8
 
9
9
  class LaravelProvider:
10
- def name(self) -> str:
10
+ def __init__(self, path: Path):
11
+ self.path = path
12
+ @classmethod
13
+ def name(cls) -> str:
11
14
  return "laravel"
12
15
 
13
- def detect(self, path: Path) -> Optional[DetectResult]:
16
+ @classmethod
17
+ def detect(cls, path: Path) -> Optional[DetectResult]:
14
18
  if _exists(path, "artisan") and _exists(path, "composer.json"):
15
- return DetectResult(self.name(), 95)
19
+ return DetectResult(cls.name(), 95)
16
20
  return None
17
21
 
18
- def initialize(self, path: Path) -> None:
22
+ def initialize(self) -> None:
19
23
  pass
20
24
 
21
- def serve_name(self, path: Path) -> str:
22
- return path.name
25
+ def serve_name(self) -> str:
26
+ return self.path.name
23
27
 
24
- def provider_kind(self, path: Path) -> str:
28
+ def provider_kind(self) -> str:
25
29
  return "php"
26
30
 
27
- def dependencies(self, path: Path) -> list[DependencySpec]:
31
+ def dependencies(self) -> list[DependencySpec]:
28
32
  return [
29
33
  DependencySpec(
30
34
  "php",
@@ -39,10 +43,10 @@ class LaravelProvider:
39
43
  DependencySpec("bash", use_in_serve=True),
40
44
  ]
41
45
 
42
- def declarations(self, path: Path) -> Optional[str]:
46
+ def declarations(self) -> Optional[str]:
43
47
  return "HOME = getenv(\"HOME\")"
44
48
 
45
- def build_steps(self, path: Path) -> list[str]:
49
+ def build_steps(self) -> list[str]:
46
50
  return [
47
51
  "env(HOME=HOME, COMPOSER_FUND=\"0\")",
48
52
  "workdir(app[\"build\"])",
@@ -53,7 +57,7 @@ class LaravelProvider:
53
57
  "run(\"pnpm run build\", outputs=[\".\"], group=\"build\")",
54
58
  ]
55
59
 
56
- def prepare_steps(self, path: Path) -> Optional[list[str]]:
60
+ def prepare_steps(self) -> Optional[list[str]]:
57
61
  return [
58
62
  'workdir(app["serve"])',
59
63
  'run("mkdir -p storage/framework/{sessions,views,cache,testing} storage/logs bootstrap/cache")',
@@ -63,17 +67,17 @@ class LaravelProvider:
63
67
  'run("php artisan view:cache")',
64
68
  ]
65
69
 
66
- def commands(self, path: Path) -> Dict[str, str]:
70
+ def commands(self) -> Dict[str, str]:
67
71
  return {
68
72
  "start": '"php -S localhost:8080 -t public"',
69
73
  "after_deploy": '"php artisan migrate"',
70
74
  }
71
75
 
72
- def assets(self, path: Path) -> Optional[Dict[str, str]]:
76
+ def assets(self) -> Optional[Dict[str, str]]:
73
77
  return None
74
78
 
75
- def mounts(self, path: Path) -> list[MountSpec]:
79
+ def mounts(self) -> list[MountSpec]:
76
80
  return [MountSpec("app")]
77
81
 
78
- def env(self, path: Path) -> Optional[Dict[str, str]]:
82
+ def env(self) -> Optional[Dict[str, str]]:
79
83
  return None
@@ -7,24 +7,28 @@ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
7
7
 
8
8
 
9
9
  class MkdocsProvider:
10
- def name(self) -> str:
10
+ def __init__(self, path: Path):
11
+ self.path = path
12
+ @classmethod
13
+ def name(cls) -> str:
11
14
  return "mkdocs"
12
15
 
13
- def detect(self, path: Path) -> Optional[DetectResult]:
16
+ @classmethod
17
+ def detect(cls, path: Path) -> Optional[DetectResult]:
14
18
  if _exists(path, "mkdocs.yml", "mkdocs.yaml"):
15
- return DetectResult(self.name(), 85)
19
+ return DetectResult(cls.name(), 85)
16
20
  return None
17
21
 
18
- def initialize(self, path: Path) -> None:
22
+ def initialize(self) -> None:
19
23
  pass
20
24
 
21
- def serve_name(self, path: Path) -> str:
22
- return path.name
25
+ def serve_name(self) -> str:
26
+ return self.path.name
23
27
 
24
- def provider_kind(self, path: Path) -> str:
28
+ def provider_kind(self) -> str:
25
29
  return "mkdocs-site"
26
30
 
27
- def dependencies(self, path: Path) -> list[DependencySpec]:
31
+ def dependencies(self) -> list[DependencySpec]:
28
32
  return [
29
33
  DependencySpec(
30
34
  "python",
@@ -46,11 +50,11 @@ class MkdocsProvider:
46
50
  ),
47
51
  ]
48
52
 
49
- def declarations(self, path: Path) -> Optional[str]:
53
+ def declarations(self) -> Optional[str]:
50
54
  return None
51
55
 
52
- def build_steps(self, path: Path) -> list[str]:
53
- has_requirements = _exists(path, "requirements.txt")
56
+ def build_steps(self) -> list[str]:
57
+ has_requirements = _exists(self.path, "requirements.txt")
54
58
  if has_requirements:
55
59
  install_lines = [
56
60
  "run(\"uv init --no-managed-python\", inputs=[], outputs=[\".\"], group=\"install\")",
@@ -68,17 +72,17 @@ class MkdocsProvider:
68
72
  "run(\"uv run mkdocs build --site-dir={}\".format(app[\"build\"]), outputs=[\".\"], group=\"build\")",
69
73
  ]
70
74
 
71
- def prepare_steps(self, path: Path) -> Optional[list[str]]:
75
+ def prepare_steps(self) -> Optional[list[str]]:
72
76
  return None
73
77
 
74
- def commands(self, path: Path) -> Dict[str, str]:
78
+ def commands(self) -> Dict[str, str]:
75
79
  return {"start": '"static-web-server --root /app"'}
76
80
 
77
- def assets(self, path: Path) -> Optional[Dict[str, str]]:
81
+ def assets(self) -> Optional[Dict[str, str]]:
78
82
  return None
79
83
 
80
- def mounts(self, path: Path) -> list[MountSpec]:
84
+ def mounts(self) -> list[MountSpec]:
81
85
  return [MountSpec("app")]
82
86
 
83
- def env(self, path: Path) -> Optional[Dict[str, str]]:
87
+ def env(self) -> Optional[Dict[str, str]]:
84
88
  return None
@@ -7,28 +7,32 @@ from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependen
7
7
 
8
8
 
9
9
  class NodeStaticProvider:
10
- def name(self) -> str:
10
+ def __init__(self, path: Path):
11
+ self.path = path
12
+ @classmethod
13
+ def name(cls) -> str:
11
14
  return "node-static"
12
15
 
13
- def detect(self, path: Path) -> Optional[DetectResult]:
16
+ @classmethod
17
+ def detect(cls, path: Path) -> Optional[DetectResult]:
14
18
  pkg = path / "package.json"
15
19
  if not pkg.exists():
16
20
  return None
17
21
  static_generators = ["astro", "vite", "next", "nuxt"]
18
22
  if any(_has_dependency(pkg, dep) for dep in static_generators):
19
- return DetectResult(self.name(), 40)
23
+ return DetectResult(cls.name(), 40)
20
24
  return None
21
25
 
22
- def initialize(self, path: Path) -> None:
26
+ def initialize(self) -> None:
23
27
  pass
24
28
 
25
- def serve_name(self, path: Path) -> str:
26
- return path.name
29
+ def serve_name(self) -> str:
30
+ return self.path.name
27
31
 
28
- def provider_kind(self, path: Path) -> str:
32
+ def provider_kind(self) -> str:
29
33
  return "staticsite"
30
34
 
31
- def dependencies(self, path: Path) -> list[DependencySpec]:
35
+ def dependencies(self) -> list[DependencySpec]:
32
36
  return [
33
37
  DependencySpec(
34
38
  "node",
@@ -40,11 +44,11 @@ class NodeStaticProvider:
40
44
  DependencySpec("static-web-server", use_in_serve=True),
41
45
  ]
42
46
 
43
- def declarations(self, path: Path) -> Optional[str]:
47
+ def declarations(self) -> Optional[str]:
44
48
  return None
45
49
 
46
- def build_steps(self, path: Path) -> list[str]:
47
- output_dir = "dist" if (path / "dist").exists() else "public"
50
+ def build_steps(self) -> list[str]:
51
+ output_dir = "dist" if (self.path / "dist").exists() else "public"
48
52
  return [
49
53
  "run(\"npm install\", inputs=[\"package.json\", \"package-lock.json\"], group=\"install\")",
50
54
  "copy(\".\", \".\", ignore=[\"node_modules\", \".git\"])",
@@ -52,18 +56,18 @@ class NodeStaticProvider:
52
56
  f"run(\"cp -R {output_dir}/* {{}}/\".format(app[\"build\"]))",
53
57
  ]
54
58
 
55
- def prepare_steps(self, path: Path) -> Optional[list[str]]:
59
+ def prepare_steps(self) -> Optional[list[str]]:
56
60
  return None
57
61
 
58
- def commands(self, path: Path) -> Dict[str, str]:
59
- output_dir = "dist" if (path / "dist").exists() else "public"
62
+ def commands(self) -> Dict[str, str]:
63
+ output_dir = "dist" if (self.path / "dist").exists() else "public"
60
64
  return {"start": f'"static-web-server --root /app/{output_dir}"'}
61
65
 
62
- def assets(self, path: Path) -> Optional[Dict[str, str]]:
66
+ def assets(self) -> Optional[Dict[str, str]]:
63
67
  return None
64
68
 
65
- def mounts(self, path: Path) -> list[MountSpec]:
69
+ def mounts(self) -> list[MountSpec]:
66
70
  return [MountSpec("app")]
67
71
 
68
- def env(self, path: Path) -> Optional[Dict[str, str]]:
72
+ def env(self) -> Optional[Dict[str, str]]:
69
73
  return None
@@ -7,29 +7,33 @@ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
7
7
 
8
8
 
9
9
  class PhpProvider:
10
- def name(self) -> str:
10
+ def __init__(self, path: Path):
11
+ self.path = path
12
+ @classmethod
13
+ def name(cls) -> str:
11
14
  return "php"
12
15
 
13
- def detect(self, path: Path) -> Optional[DetectResult]:
16
+ @classmethod
17
+ def detect(cls, path: Path) -> Optional[DetectResult]:
14
18
  if _exists(path, "composer.json") and _exists(path, "public/index.php"):
15
- return DetectResult(self.name(), 60)
19
+ return DetectResult(cls.name(), 60)
16
20
  if _exists(path, "index.php") and not _exists(path, "composer.json"):
17
- return DetectResult(self.name(), 10)
21
+ return DetectResult(cls.name(), 10)
18
22
  return None
19
23
 
20
- def initialize(self, path: Path) -> None:
24
+ def initialize(self) -> None:
21
25
  pass
22
26
 
23
- def serve_name(self, path: Path) -> str:
24
- return path.name
27
+ def serve_name(self) -> str:
28
+ return self.path.name
25
29
 
26
- def provider_kind(self, path: Path) -> str:
30
+ def provider_kind(self) -> str:
27
31
  return "php"
28
32
 
29
- def has_composer(self, path: Path) -> bool:
30
- return _exists(path, "composer.json", "composer.lock")
33
+ def has_composer(self) -> bool:
34
+ return _exists(self.path, "composer.json", "composer.lock")
31
35
 
32
- def dependencies(self, path: Path) -> list[DependencySpec]:
36
+ def dependencies(self) -> list[DependencySpec]:
33
37
  deps = [
34
38
  DependencySpec(
35
39
  "php",
@@ -39,40 +43,40 @@ class PhpProvider:
39
43
  use_in_serve=True,
40
44
  ),
41
45
  ]
42
- if self.has_composer(path):
46
+ if self.has_composer():
43
47
  deps.append(DependencySpec("composer", use_in_build=True))
44
48
  deps.append(DependencySpec("bash", use_in_serve=True))
45
49
  return deps
46
50
 
47
- def declarations(self, path: Path) -> Optional[str]:
51
+ def declarations(self) -> Optional[str]:
48
52
  return "HOME = getenv(\"HOME\")"
49
53
 
50
- def build_steps(self, path: Path) -> list[str]:
54
+ def build_steps(self) -> list[str]:
51
55
  steps = [
52
56
  "workdir(app[\"build\"])",
53
57
  ]
54
58
 
55
- if self.has_composer(path):
59
+ if self.has_composer():
56
60
  steps.append("env(HOME=HOME, COMPOSER_FUND=\"0\")")
57
61
  steps.append("run(\"composer install --optimize-autoloader --no-scripts --no-interaction\", inputs=[\"composer.json\", \"composer.lock\"], outputs=[\".\"], group=\"install\")")
58
62
 
59
63
  steps.append("copy(\".\", \".\", ignore=[\".git\"])")
60
64
  return steps
61
65
 
62
- def prepare_steps(self, path: Path) -> Optional[list[str]]:
66
+ def prepare_steps(self) -> Optional[list[str]]:
63
67
  return None
64
68
 
65
- def commands(self, path: Path) -> Dict[str, str]:
66
- if _exists(path, "public/index.php"):
69
+ def commands(self) -> Dict[str, str]:
70
+ if _exists(self.path, "public/index.php"):
67
71
  return {"start": '"php -S localhost:8080 -t public"'}
68
- elif _exists(path, "index.php"):
72
+ elif _exists(self.path, "index.php"):
69
73
  return {"start": '"php -S localhost:8080" -t .'}
70
74
 
71
- def assets(self, path: Path) -> Optional[Dict[str, str]]:
75
+ def assets(self) -> Optional[Dict[str, str]]:
72
76
  return {"php.ini": "get_asset(\"php/php.ini\")"}
73
77
 
74
- def mounts(self, path: Path) -> list[MountSpec]:
78
+ def mounts(self) -> list[MountSpec]:
75
79
  return [MountSpec("app")]
76
80
 
77
- def env(self, path: Path) -> Optional[Dict[str, str]]:
81
+ def env(self) -> Optional[Dict[str, str]]:
78
82
  return None
@@ -0,0 +1,309 @@
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
+ f"run(\"uv add {extra_deps}\", inputs=[\"pyproject.toml\"], group=\"install\")" if extra_deps else None,
215
+ "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",
216
+ 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",
217
+ "run(\"rm cross-requirements.txt\") if cross_platform else None",
218
+ ]))
219
+ if _exists(self.path, "requirements.txt"):
220
+ steps += [
221
+ "env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
222
+ "run(f\"uv init --no-managed-python --python python{python_version}\", inputs=[], outputs=[\"uv.lock\"], group=\"install\")",
223
+ "run(f\"uv add -r requirements.txt\", inputs=[\"requirements.txt\"], group=\"install\")",
224
+ "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",
225
+ "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",
226
+ "run(\"rm cross-requirements.txt\") if cross_platform else None",
227
+ ]
228
+
229
+ steps += [
230
+ "path((local_venv[\"build\"] if cross_platform else venv[\"build\"]) + \"/bin\")",
231
+ "copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
232
+ ]
233
+ return steps
234
+
235
+ def prepare_steps(self) -> Optional[list[str]]:
236
+ return [
237
+ 'run("echo \\\"Precompiling Python code...\\\"") if precompile_python else None',
238
+ 'run(f"python -m compileall -o 2 {python_serve_path}") if precompile_python else None',
239
+ 'run("echo \\\"Precompiling package code...\\\"") if precompile_python else None',
240
+ 'run("python -m compileall -o 2 {}".format(app["serve"])) if precompile_python else None',
241
+ ]
242
+
243
+ def commands(self) -> Dict[str, str]:
244
+ if self.framework == PythonFramework.Django:
245
+ start_cmd = None
246
+ if self.server == PythonServer.Daphne and self.asgi_application:
247
+ asgi_application = format_app_import(self.asgi_application)
248
+ start_cmd = f'"python -m daphne {asgi_application} --bind 0.0.0.0 --port 8000"'
249
+ elif self.server == PythonServer.Uvicorn:
250
+ if self.asgi_application:
251
+ asgi_application = format_app_import(self.asgi_application)
252
+ start_cmd = f'"python -m uvicorn {asgi_application} --host 0.0.0.0 --port 8000"'
253
+ elif self.wsgi_application:
254
+ wsgi_application = format_app_import(self.wsgi_application)
255
+ start_cmd = f'"python -m uvicorn {wsgi_application} --interface=wsgi --host 0.0.0.0 --port 8000"'
256
+ # elif self.server == PythonServer.Gunicorn:
257
+ # start_cmd = f'"python -m gunicorn {self.wsgi_application} --bind 0.0.0.0 --port 8000"'
258
+ if not start_cmd:
259
+ # We run the default runserver command if no server is specified
260
+ start_cmd = '"python manage.py runserver 0.0.0.0:8000"'
261
+ migrate_cmd = '"python manage.py migrate"'
262
+ return {"start": start_cmd, "after_deploy": migrate_cmd}
263
+ elif self.framework == PythonFramework.FastAPI:
264
+ if _exists(self.path, "main.py"):
265
+ path = "main:app"
266
+ elif _exists(self.path, "src/main.py"):
267
+ path = "src.main:app"
268
+
269
+ if self.server == PythonServer.Uvicorn:
270
+ start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
271
+ elif self.server == PythonServer.Hypercorn:
272
+ start_cmd = f'"python -m hypercorn {path} --bind 0.0.0.0:8000"'
273
+ else:
274
+ start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
275
+ return {"start": start_cmd}
276
+ elif self.framework == PythonFramework.FastHTML:
277
+ if _exists(self.path, "main.py"):
278
+ path = "main:app"
279
+ elif _exists(self.path, "src/main.py"):
280
+ path = "src.main:app"
281
+ start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
282
+ elif _exists(self.path, "main.py"):
283
+ start_cmd = '"python main.py"'
284
+ elif _exists(self.path, "src/main.py"):
285
+ start_cmd = '"python src/main.py"'
286
+ else:
287
+ start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
288
+ return {"start": start_cmd}
289
+
290
+ def assets(self) -> Optional[Dict[str, str]]:
291
+ return None
292
+
293
+ def mounts(self) -> list[MountSpec]:
294
+ return [
295
+ MountSpec("app"),
296
+ MountSpec("venv"),
297
+ MountSpec("local_venv", attach_to_serve=False),
298
+ ]
299
+
300
+ def env(self) -> Optional[Dict[str, str]]:
301
+ # For Django projects, generate an empty env dict to surface the field
302
+ # in the Shipit file. Other Python projects omit it by default.
303
+ return {
304
+ "PYTHONPATH": "python_serve_path"
305
+ }
306
+
307
+ def format_app_import(asgi_application: str) -> str:
308
+ # Transform "mysite.asgi.application" to "mysite.asgi:application" using regex
309
+ return re.sub(r"\.([^.]+)$", r":\1", asgi_application)
@@ -11,16 +11,15 @@ from .python import PythonProvider
11
11
  from .staticfile import StaticFileProvider
12
12
 
13
13
 
14
- def providers() -> list[Provider]:
14
+ def providers() -> list[type[Provider]]:
15
15
  # Order matters: more specific providers first
16
16
  return [
17
- LaravelProvider(),
18
- GatsbyProvider(),
19
- HugoProvider(),
20
- MkdocsProvider(),
21
- PythonProvider(),
22
- PhpProvider(),
23
- NodeStaticProvider(),
24
- StaticFileProvider(),
17
+ LaravelProvider,
18
+ GatsbyProvider,
19
+ HugoProvider,
20
+ MkdocsProvider,
21
+ PythonProvider,
22
+ PhpProvider,
23
+ NodeStaticProvider,
24
+ StaticFileProvider,
25
25
  ]
26
-
@@ -7,28 +7,33 @@ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
7
7
 
8
8
 
9
9
  class StaticFileProvider:
10
- def name(self) -> str:
10
+ def __init__(self, path: Path):
11
+ self.path = path
12
+
13
+ @classmethod
14
+ def name(cls) -> str:
11
15
  return "staticfile"
12
16
 
13
- def detect(self, path: Path) -> Optional[DetectResult]:
17
+ @classmethod
18
+ def detect(cls, path: Path) -> Optional[DetectResult]:
14
19
  if _exists(path, "Staticfile"):
15
- return DetectResult(self.name(), 50)
20
+ return DetectResult(cls.name(), 50)
16
21
  if _exists(path, "index.html") and not _exists(
17
22
  path, "package.json", "pyproject.toml", "composer.json"
18
23
  ):
19
- return DetectResult(self.name(), 10)
24
+ return DetectResult(cls.name(), 10)
20
25
  return None
21
26
 
22
- def initialize(self, path: Path) -> None:
27
+ def initialize(self) -> None:
23
28
  pass
24
29
 
25
- def serve_name(self, path: Path) -> str:
26
- return path.name
30
+ def serve_name(self) -> str:
31
+ return self.path.name
27
32
 
28
- def provider_kind(self, path: Path) -> str:
33
+ def provider_kind(self) -> str:
29
34
  return "staticfile"
30
35
 
31
- def dependencies(self, path: Path) -> list[DependencySpec]:
36
+ def dependencies(self) -> list[DependencySpec]:
32
37
  return [
33
38
  DependencySpec(
34
39
  "static-web-server",
@@ -38,28 +43,28 @@ class StaticFileProvider:
38
43
  )
39
44
  ]
40
45
 
41
- def build_steps(self, path: Path) -> list[str]:
46
+ def build_steps(self) -> list[str]:
42
47
  return [
43
48
  'workdir(app["build"])',
44
49
  'copy(".", ".", ignore=[".git"])'
45
50
  ]
46
51
 
47
- def prepare_steps(self, path: Path) -> Optional[list[str]]:
52
+ def prepare_steps(self) -> Optional[list[str]]:
48
53
  return None
49
54
 
50
- def declarations(self, path: Path) -> Optional[str]:
55
+ def declarations(self) -> Optional[str]:
51
56
  return None
52
57
 
53
- def commands(self, path: Path) -> Dict[str, str]:
58
+ def commands(self) -> Dict[str, str]:
54
59
  return {
55
60
  "start": '"static-web-server --root={} --log-level=info".format(app["serve"])'
56
61
  }
57
62
 
58
- def assets(self, path: Path) -> Optional[Dict[str, str]]:
63
+ def assets(self) -> Optional[Dict[str, str]]:
59
64
  return None
60
65
 
61
- def mounts(self, path: Path) -> list[MountSpec]:
66
+ def mounts(self) -> list[MountSpec]:
62
67
  return [MountSpec("app")]
63
68
 
64
- def env(self, path: Path) -> Optional[Dict[str, str]]:
69
+ def env(self) -> Optional[Dict[str, str]]:
65
70
  return None
@@ -0,0 +1,5 @@
1
+ __all__ = ["version", "version_info"]
2
+
3
+
4
+ version = "0.4.0"
5
+ version_info = (0, 4, 0, "final", 0)
@@ -1,135 +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 name(self) -> str:
17
- return "python"
18
-
19
- def detect(self, path: Path) -> Optional[DetectResult]:
20
- if _exists(path, "pyproject.toml", "requirements.txt"):
21
- if _exists(path, "manage.py"):
22
- return DetectResult(self.name(), 70)
23
- return DetectResult(self.name(), 50)
24
- return None
25
-
26
- def initialize(self, path: Path) -> None:
27
- pass
28
-
29
- def serve_name(self, path: Path) -> str:
30
- return path.name
31
-
32
- def provider_kind(self, path: Path) -> str:
33
- return "python"
34
-
35
- def dependencies(self, path: Path) -> list[DependencySpec]:
36
- if _exists(path, ".python-version"):
37
- python_version = (path / ".python-version").read_text().strip()
38
- else:
39
- python_version = "3.13"
40
-
41
- return [
42
- DependencySpec(
43
- "python",
44
- env_var="SHIPIT_PYTHON_VERSION",
45
- default_version=python_version,
46
- use_in_build=True,
47
- use_in_serve=True,
48
- ),
49
- DependencySpec(
50
- "uv",
51
- env_var="SHIPIT_UV_VERSION",
52
- default_version="0.8.15",
53
- use_in_build=True,
54
- ),
55
- ]
56
-
57
- def declarations(self, path: Path) -> Optional[str]:
58
- return (
59
- "cross_platform = getenv(\"SHIPIT_PYTHON_CROSS_PLATFORM\")\n"
60
- "python_extra_index_url = getenv(\"SHIPIT_PYTHON_EXTRA_INDEX_URL\")\n"
61
- "precompile_python = getenv(\"SHIPIT_PYTHON_PRECOMPILE\") in [\"true\", \"True\", \"TRUE\", \"1\", \"on\", \"yes\", \"y\", \"Y\", \"YES\", \"On\", \"ON\"]\n"
62
- "python_cross_packages_path = venv[\"build\"] + f\"/lib/python{python_version}/site-packages\""
63
- )
64
-
65
- def build_steps(self, path: Path) -> list[str]:
66
- steps = [
67
- "workdir(app[\"build\"])"
68
- ]
69
-
70
- if _exists(path, "pyproject.toml"):
71
- input_files = ["pyproject.toml"]
72
- if _exists(path, "uv.lock"):
73
- input_files.append("uv.lock")
74
- inputs = ", ".join([f"\"{input}\"" for input in input_files])
75
- steps += [
76
- "env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
77
- "run(f\"uv sync --compile --python python{python_version} --locked --no-managed-python\", inputs=[" + inputs + "], group=\"install\")",
78
- "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",
79
- "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",
80
- "run(\"rm cross-requirements.txt\") if cross_platform else None",
81
- ]
82
- if _exists(path, "requirements.txt"):
83
- steps += [
84
- "env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
85
- "run(f\"uv init --no-managed-python --python python{python_version}\", inputs=[], outputs=[\"uv.lock\"], group=\"install\")",
86
- "run(f\"uv add -r requirements.txt\", inputs=[\"requirements.txt\"], group=\"install\")",
87
- "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",
88
- "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",
89
- "run(\"rm cross-requirements.txt\") if cross_platform else None",
90
- ]
91
-
92
- steps += [
93
- "path((local_venv[\"build\"] if cross_platform else venv[\"build\"]) + \"/bin\")",
94
- "copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
95
- ]
96
- return steps
97
-
98
- def prepare_steps(self, path: Path) -> Optional[list[str]]:
99
- return [
100
- 'workdir(app["serve"])',
101
- 'run("echo \\\"Precompiling Python code...\\\"") if precompile_python else None',
102
- 'run("python -m compileall -o 2 $PYTHONPATH") if precompile_python else None',
103
- 'run("echo \\\"Precompiling package code...\\\"") if precompile_python else None',
104
- 'run("python -m compileall -o 2 .") if precompile_python else None',
105
- ]
106
-
107
- def commands(self, path: Path) -> Dict[str, str]:
108
- if _exists(path, "manage.py"):
109
- start_cmd = '"python manage.py runserver 0.0.0.0:8000"'
110
- migrate_cmd = '"python manage.py migrate"'
111
- return {"start": start_cmd, "after_deploy": migrate_cmd}
112
- elif _exists(path, "main.py"):
113
- start_cmd = '"python main.py"'
114
- elif _exists(path, "src/main.py"):
115
- start_cmd = '"python src/main.py"'
116
- else:
117
- start_cmd = '"python -c \'print(\\\"Hello, World!\\\")\'"'
118
- return {"start": start_cmd}
119
-
120
- def assets(self, path: Path) -> Optional[Dict[str, str]]:
121
- return None
122
-
123
- def mounts(self, path: Path) -> list[MountSpec]:
124
- return [
125
- MountSpec("app"),
126
- MountSpec("venv"),
127
- MountSpec("local_venv", attach_to_serve=False),
128
- ]
129
-
130
- def env(self, path: Path) -> Optional[Dict[str, str]]:
131
- # For Django projects, generate an empty env dict to surface the field
132
- # in the Shipit file. Other Python projects omit it by default.
133
- return {
134
- "PYTHONPATH": "\"{}/lib/python{}/site-packages\".format(venv[\"serve\"], python_version)"
135
- }
@@ -1,5 +0,0 @@
1
- __all__ = ["version", "version_info"]
2
-
3
-
4
- version = "0.3.3"
5
- version_info = (0, 3, 3, "final", 0)
File without changes
File without changes