shipit-cli 0.5.1__tar.gz → 0.6.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.5.1 → shipit_cli-0.6.0}/PKG-INFO +1 -1
  2. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/pyproject.toml +1 -1
  3. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/cli.py +104 -13
  4. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/generator.py +10 -0
  5. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/providers/base.py +9 -1
  6. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/providers/gatsby.py +4 -1
  7. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/providers/hugo.py +4 -1
  8. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/providers/laravel.py +4 -1
  9. shipit_cli-0.6.0/src/shipit/providers/mkdocs.py +63 -0
  10. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/providers/node_static.py +4 -1
  11. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/providers/php.py +4 -1
  12. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/providers/python.py +191 -91
  13. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/providers/staticfile.py +4 -1
  14. shipit_cli-0.6.0/src/shipit/version.py +5 -0
  15. shipit_cli-0.5.1/src/shipit/providers/mkdocs.py +0 -88
  16. shipit_cli-0.5.1/src/shipit/version.py +0 -5
  17. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/.gitignore +0 -0
  18. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/README.md +0 -0
  19. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/__init__.py +0 -0
  20. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/assets/php/php.ini +0 -0
  21. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/src/shipit/providers/registry.py +0 -0
  22. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/tests/test_examples_build.py +0 -0
  23. {shipit_cli-0.5.1 → shipit_cli-0.6.0}/tests/test_generate_shipit_examples.py +0 -0
  24. {shipit_cli-0.5.1 → shipit_cli-0.6.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.5.1
3
+ Version: 0.6.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.5.1"
3
+ version = "0.6.0"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -16,6 +16,7 @@ from typing import (
16
16
  Set,
17
17
  TypedDict,
18
18
  Union,
19
+ Literal,
19
20
  cast,
20
21
  )
21
22
  from shutil import copy, copytree, ignore_patterns
@@ -48,6 +49,12 @@ class Mount:
48
49
  serve_path: Path
49
50
 
50
51
 
52
+ @dataclass
53
+ class Service:
54
+ name: str
55
+ provider: Literal["postgres", "mysql", "redis"] # Right now we only support postgres and mysql
56
+
57
+
51
58
  @dataclass
52
59
  class Serve:
53
60
  name: str
@@ -61,6 +68,7 @@ class Serve:
61
68
  workers: Optional[List[str]] = None
62
69
  mounts: Optional[List[Mount]] = None
63
70
  env: Optional[Dict[str, str]] = None
71
+ services: Optional[List[Service]] = None
64
72
 
65
73
 
66
74
  @dataclass
@@ -594,10 +602,10 @@ class LocalBuilder:
594
602
  commands.append(step.command)
595
603
  elif isinstance(step, WorkdirStep):
596
604
  commands.append(f"cd {step.path}")
597
- content = "#!/bin/bash\n{body}".format(
598
- body="\n".join(commands)
605
+ content = "#!/bin/bash\n{body}".format(body="\n".join(commands))
606
+ console.print(
607
+ f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]"
599
608
  )
600
- console.print(f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]")
601
609
  manifest_panel = Panel(
602
610
  Syntax(
603
611
  content,
@@ -624,20 +632,31 @@ class LocalBuilder:
624
632
 
625
633
  def build_serve(self, serve: Serve) -> None:
626
634
  console.print("\n[bold]Building serve[/bold]")
627
- build_path = self.get_build_path()
628
635
  serve_command_path = self.get_serve_path() / "bin"
629
636
  serve_command_path.mkdir(parents=True, exist_ok=False)
630
637
  path = self.get_path() / ".path"
631
- # path_resolved = [str((build_path/path).resolve()) for path in path.read_text().split(os.pathsep) if path]
632
- # path_text = os.pathsep.join(path_resolved)
633
638
  path_text = path.read_text()
634
639
  console.print(f"[bold]Serve Commands:[/bold]")
635
640
  for command in serve.commands:
636
641
  console.print(f"* {command}")
637
642
  command_path = serve_command_path / command
643
+ content = f"#!/bin/bash\ncd {serve.cwd}\nPATH={path_text}:$PATH {serve.commands[command]}"
638
644
  command_path.write_text(
639
- f"#!/bin/bash\ncd {build_path}\nPATH={path_text}:$PATH {serve.commands[command]}"
645
+ content
646
+ )
647
+ manifest_panel = Panel(
648
+ Syntax(
649
+ content.strip(),
650
+ "bash",
651
+ theme="monokai",
652
+ background_color="default",
653
+ line_numbers=True,
654
+ ),
655
+ box=box.SQUARE,
656
+ border_style="bright_black",
657
+ expand=False,
640
658
  )
659
+ console.print(manifest_panel, markup=False, highlight=True)
641
660
  command_path.chmod(0o755)
642
661
 
643
662
  def run_serve_command(self, command: str) -> None:
@@ -669,8 +688,8 @@ class WasmerBuilder:
669
688
  mapper: Dict[str, MapperItem] = {
670
689
  "python": {
671
690
  "dependencies": {
672
- "latest": "wasmer/python-native@=0.1.11",
673
- "3.13": "wasmer/python-native@=0.1.11",
691
+ "latest": "python/python@=3.13.1",
692
+ "3.13": "python/python@=3.13.1",
674
693
  },
675
694
  "scripts": {"python"},
676
695
  "aliases": {},
@@ -679,6 +698,20 @@ class WasmerBuilder:
679
698
  "PYTHONHOME": "/cpython",
680
699
  },
681
700
  },
701
+ "pandoc": {
702
+ "dependencies": {
703
+ "latest": "wasmer/pandoc@=0.0.1",
704
+ "3.5": "wasmer/pandoc@=0.0.1",
705
+ },
706
+ "scripts": {"pandoc"},
707
+ },
708
+ "ffmpeg": {
709
+ "dependencies": {
710
+ "latest": "wasmer/ffmpeg@=1.0.5",
711
+ "N-111519": "wasmer/ffmpeg@=1.0.5",
712
+ },
713
+ "scripts": {"ffmpeg"},
714
+ },
682
715
  "php": {
683
716
  "dependencies": {
684
717
  "latest": "php/php-32@=8.3.2104",
@@ -773,7 +806,9 @@ class WasmerBuilder:
773
806
 
774
807
  body = "\n".join(filter(None, [env_lines, *commands]))
775
808
  content = f"#!/bin/bash\n\n{body}"
776
- console.print(f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]")
809
+ console.print(
810
+ f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]"
811
+ )
777
812
  manifest_panel = Panel(
778
813
  Syntax(
779
814
  content,
@@ -840,12 +875,14 @@ class WasmerBuilder:
840
875
  version
841
876
  ].split("@")
842
877
  dependencies.add(package_name, version)
843
- for script in self.mapper[dep.name]["scripts"]:
878
+ scripts = self.mapper[dep.name].get("scripts") or []
879
+ for script in scripts:
844
880
  binaries[script] = {
845
881
  "script": f"{package_name}:{script}",
846
882
  "env": self.mapper[dep.name].get("env"),
847
883
  }
848
- for alias, script in self.mapper[dep.name]["aliases"].items():
884
+ aliases = self.mapper[dep.name].get("aliases") or {}
885
+ for alias, script in aliases.items():
849
886
  binaries[alias] = {
850
887
  "script": f"{package_name}:{script}",
851
888
  "env": self.mapper[dep.name].get("env"),
@@ -922,7 +959,9 @@ class WasmerBuilder:
922
959
 
923
960
  original_app_yaml_path = self.src_dir / "app.yaml"
924
961
  if original_app_yaml_path.exists():
925
- console.print(f"[bold]Using original app.yaml found in source directory[/bold]")
962
+ console.print(
963
+ f"[bold]Using original app.yaml found in source directory[/bold]"
964
+ )
926
965
  yaml_config = yaml.safe_load(original_app_yaml_path.read_text())
927
966
  else:
928
967
  yaml_config = {
@@ -930,8 +969,46 @@ class WasmerBuilder:
930
969
  }
931
970
  # Update the app to use the new package
932
971
  yaml_config["package"] = "."
972
+ if serve.services:
973
+ capabilities = yaml_config.get("capabilities", {})
974
+ has_mysql = any(service.provider == "mysql" for service in serve.services)
975
+ # has_postgres = any(service.provider == "postgres" for service in serve.services)
976
+ # has_redis = any(service.provider == "redis" for service in serve.services)
977
+ if has_mysql:
978
+ capabilities["database"] = {
979
+ "engine": "mysql"
980
+ }
981
+ yaml_config["capabilities"] = capabilities
982
+
983
+ if "after_deploy" in serve.commands:
984
+ jobs = yaml_config.get("jobs", [])
985
+ jobs.append({
986
+ "name": "after_deploy",
987
+ "trigger": "post-deployment",
988
+ "action": {
989
+ "execute": {
990
+ "command": "after_deploy"
991
+ }
992
+ }
993
+ })
994
+ yaml_config["jobs"] = jobs
933
995
 
934
996
  app_yaml = yaml.dump(yaml_config)
997
+
998
+ console.print(f"\n[bold]Created app.yaml manifest ✅[/bold]")
999
+ app_yaml_panel = Panel(
1000
+ Syntax(
1001
+ app_yaml.strip(),
1002
+ "yaml",
1003
+ theme="monokai",
1004
+ background_color="default",
1005
+ line_numbers=True,
1006
+ ),
1007
+ box=box.SQUARE,
1008
+ border_style="bright_black",
1009
+ expand=False,
1010
+ )
1011
+ console.print(app_yaml_panel, markup=False, highlight=True)
935
1012
  (self.wasmer_dir_path / "app.yaml").write_text(app_yaml)
936
1013
 
937
1014
  # self.inner_builder.build_serve(serve)
@@ -1013,12 +1090,17 @@ class Ctx:
1013
1090
  self.steps: List[Step] = []
1014
1091
  self.serves: Dict[str, Serve] = {}
1015
1092
  self.mounts: List[Mount] = []
1093
+ self.services: Dict[str, Service] = {}
1016
1094
 
1017
1095
  def add_package(self, package: Package) -> str:
1018
1096
  index = f"{package.name}@{package.version}" if package.version else package.name
1019
1097
  self.packages[index] = package
1020
1098
  return f"ref:package:{index}"
1021
1099
 
1100
+ def add_service(self, service: Service) -> str:
1101
+ self.services[service.name] = service
1102
+ return f"ref:service:{service.name}"
1103
+
1022
1104
  def get_ref(self, index: str) -> Any:
1023
1105
  if index.startswith("ref:package:"):
1024
1106
  return self.packages[index[len("ref:package:") :]]
@@ -1030,6 +1112,8 @@ class Ctx:
1030
1112
  return self.steps[int(index[len("ref:step:") :])]
1031
1113
  elif index.startswith("ref:mount:"):
1032
1114
  return self.mounts[int(index[len("ref:mount:") :])]
1115
+ elif index.startswith("ref:service:"):
1116
+ return self.services[index[len("ref:service:") :]]
1033
1117
  else:
1034
1118
  raise Exception(f"Invalid reference: {index}")
1035
1119
 
@@ -1059,6 +1143,10 @@ class Ctx:
1059
1143
  def dep(self, name: str, version: Optional[str] = None) -> str:
1060
1144
  package = Package(name, version)
1061
1145
  return self.add_package(package)
1146
+
1147
+ def service(self, name: str, provider: Literal["postgres", "mysql", "redis"]) -> str:
1148
+ service = Service(name, provider)
1149
+ return self.add_service(service)
1062
1150
 
1063
1151
  def serve(
1064
1152
  self,
@@ -1073,6 +1161,7 @@ class Ctx:
1073
1161
  workers: Optional[List[str]] = None,
1074
1162
  mounts: Optional[List[Mount]] = None,
1075
1163
  env: Optional[Dict[str, str]] = None,
1164
+ services: Optional[List[str]] = None,
1076
1165
  ) -> str:
1077
1166
  build_refs = [cast(Step, r) for r in self.get_refs(build)]
1078
1167
  prepare_steps: Optional[List[PrepareStep]] = None
@@ -1097,6 +1186,7 @@ class Ctx:
1097
1186
  if mounts
1098
1187
  else None,
1099
1188
  env=env,
1189
+ services=self.get_refs(services) if services else None,
1100
1190
  )
1101
1191
  return self.add_serve(serve)
1102
1192
 
@@ -1422,6 +1512,7 @@ def build(
1422
1512
  glb = sl.Globals.standard()
1423
1513
  mod = sl.Module()
1424
1514
 
1515
+ mod.add_callable("service", ctx.service)
1425
1516
  mod.add_callable("getenv", ctx.getenv)
1426
1517
  mod.add_callable("dep", ctx.dep)
1427
1518
  mod.add_callable("serve", ctx.serve)
@@ -89,6 +89,7 @@ def generate_shipit(path: Path) -> str:
89
89
  dependencies=provider.dependencies(),
90
90
  build_steps=provider.build_steps(),
91
91
  prepare=provider.prepare_steps(),
92
+ services=provider.services(),
92
93
  commands=provider.commands(),
93
94
  env=provider.env(),
94
95
  )
@@ -127,6 +128,10 @@ def generate_shipit(path: Path) -> str:
127
128
  out.append("")
128
129
  for m in plan.mounts:
129
130
  out.append(f"{m.name} = mount(\"{m.name}\")")
131
+ if plan.services:
132
+ for s in plan.services:
133
+ out.append(f"{s.name} = service(\n name=\"{s.name}\",\n provider=\"{s.provider}\"\n)")
134
+
130
135
  if plan.declarations:
131
136
  out.append(plan.declarations)
132
137
  out.append("")
@@ -157,6 +162,11 @@ def generate_shipit(path: Path) -> str:
157
162
  out.append(" commands = {")
158
163
  out.append(commands_lines)
159
164
  out.append(" },")
165
+ if plan.services:
166
+ out.append(" services=[")
167
+ for s in plan.services:
168
+ out.append(f" {s.name},")
169
+ out.append(" ],")
160
170
  if mounts_block:
161
171
  out.append(" mounts=[")
162
172
  out.append(mounts_block)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
- from typing import Dict, List, Optional, Protocol
5
+ from typing import Dict, List, Optional, Protocol, Literal
6
6
 
7
7
 
8
8
  @dataclass
@@ -28,6 +28,7 @@ class Provider(Protocol):
28
28
  def prepare_steps(self) -> Optional[List[str]]: ...
29
29
  def commands(self) -> Dict[str, str]: ...
30
30
  def assets(self) -> Optional[Dict[str, str]]: ...
31
+ def services(self) -> List["ServiceSpec"]: ...
31
32
  def mounts(self) -> List["MountSpec"]: ...
32
33
  def env(self) -> Optional[Dict[str, str]]: ...
33
34
 
@@ -49,6 +50,12 @@ class MountSpec:
49
50
  attach_to_serve: bool = True
50
51
 
51
52
 
53
+ @dataclass
54
+ class ServiceSpec:
55
+ name: str
56
+ provider: Literal["postgres", "mysql", "redis"]
57
+
58
+
52
59
  @dataclass
53
60
  class ProviderPlan:
54
61
  serve_name: str
@@ -58,6 +65,7 @@ class ProviderPlan:
58
65
  dependencies: List[DependencySpec] = field(default_factory=list)
59
66
  build_steps: List[str] = field(default_factory=list)
60
67
  prepare: Optional[List[str]] = None
68
+ services: List[ServiceSpec] = field(default_factory=list)
61
69
  commands: Dict[str, str] = field(default_factory=dict)
62
70
  assets: Optional[Dict[str, str]] = None
63
71
  env: Optional[Dict[str, str]] = None
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Dict, Optional
5
5
 
6
- from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependency, MountSpec
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependency, MountSpec, ServiceSpec
7
7
 
8
8
 
9
9
  class GatsbyProvider:
@@ -70,3 +70,6 @@ class GatsbyProvider:
70
70
 
71
71
  def env(self) -> Optional[Dict[str, str]]:
72
72
  return None
73
+
74
+ def services(self) -> list[ServiceSpec]:
75
+ return []
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Dict, Optional
5
5
 
6
- from .base import DetectResult, DependencySpec, Provider, _exists
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, ServiceSpec
7
7
  from .staticfile import StaticFileProvider
8
8
 
9
9
  class HugoProvider(StaticFileProvider):
@@ -45,3 +45,6 @@ class HugoProvider(StaticFileProvider):
45
45
  'copy(".", ".", ignore=[".git"])',
46
46
  'run("hugo build --destination={}".format(app["build"]), group="build")',
47
47
  ]
48
+
49
+ def services(self) -> list[ServiceSpec]:
50
+ return []
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Dict, Optional
5
5
 
6
- from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec, ServiceSpec
7
7
 
8
8
 
9
9
  class LaravelProvider:
@@ -81,3 +81,6 @@ class LaravelProvider:
81
81
 
82
82
  def env(self) -> Optional[Dict[str, str]]:
83
83
  return None
84
+
85
+ def services(self) -> list[ServiceSpec]:
86
+ return []
@@ -0,0 +1,63 @@
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, ServiceSpec
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()
61
+
62
+ def services(self) -> list[ServiceSpec]:
63
+ return []
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Dict, Optional
5
5
 
6
- from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependency, MountSpec
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependency, MountSpec, ServiceSpec
7
7
 
8
8
 
9
9
  class NodeStaticProvider:
@@ -71,3 +71,6 @@ class NodeStaticProvider:
71
71
 
72
72
  def env(self) -> Optional[Dict[str, str]]:
73
73
  return None
74
+
75
+ def services(self) -> list[ServiceSpec]:
76
+ return []
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Dict, Optional
5
5
 
6
- from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec, ServiceSpec
7
7
 
8
8
 
9
9
  class PhpProvider:
@@ -80,3 +80,6 @@ class PhpProvider:
80
80
 
81
81
  def env(self) -> Optional[Dict[str, str]]:
82
82
  return None
83
+
84
+ def services(self) -> list[ServiceSpec]:
85
+ return []
@@ -11,6 +11,7 @@ from .base import (
11
11
  Provider,
12
12
  _exists,
13
13
  MountSpec,
14
+ ServiceSpec,
14
15
  )
15
16
 
16
17
 
@@ -29,6 +30,7 @@ class PythonServer(Enum):
29
30
  # Gunicorn = "gunicorn"
30
31
  Daphne = "daphne"
31
32
 
33
+
32
34
  class DatabaseType(Enum):
33
35
  MySQL = "mysql"
34
36
  PostgreSQL = "postgresql"
@@ -41,8 +43,17 @@ class PythonProvider:
41
43
  extra_dependencies: Set[str]
42
44
  asgi_application: Optional[str] = None
43
45
  wsgi_application: Optional[str] = None
44
-
45
- def __init__(self, path: Path):
46
+ uses_ffmpeg: bool = False
47
+ uses_pandoc: bool = False
48
+ only_build: bool = False
49
+ install_requires_all_files: bool = False
50
+
51
+ def __init__(
52
+ self,
53
+ path: Path,
54
+ only_build: bool = False,
55
+ extra_dependencies: Optional[Set[str]] = None,
56
+ ):
46
57
  self.path = path
47
58
  if _exists(self.path, ".python-version"):
48
59
  python_version = (self.path / ".python-version").read_text().strip()
@@ -50,6 +61,11 @@ class PythonProvider:
50
61
  python_version = "3.13"
51
62
  self.default_python_version = python_version
52
63
  self.extra_dependencies = set()
64
+ self.only_build = only_build
65
+ self.extra_dependencies = extra_dependencies or set()
66
+
67
+ if self.only_build:
68
+ return
53
69
 
54
70
  pg_deps = {
55
71
  "asyncpg",
@@ -57,9 +73,11 @@ class PythonProvider:
57
73
  "psycopg",
58
74
  "psycopg2",
59
75
  "psycopg-binary",
60
- "psycopg2-binary"}
61
- mysql_deps = {"mysqlclient", "pymysql", "mysql-connector-python", "aiomysql"}
76
+ "psycopg2-binary",
77
+ }
78
+ mysql_deps = {"mysqlclient", "pymysql", "mysql-connector-python", "aiomysql", "asyncmy"}
62
79
  found_deps = self.check_deps(
80
+ "file://", # This is not really a dependency, but as a way to check if the install script requires all files
63
81
  "streamlit",
64
82
  "django",
65
83
  "mcp",
@@ -69,11 +87,17 @@ class PythonProvider:
69
87
  "daphne",
70
88
  "hypercorn",
71
89
  "uvicorn",
90
+ # Other
91
+ "ffmpeg",
92
+ "pandoc",
72
93
  # "gunicorn",
73
94
  *mysql_deps,
74
95
  *pg_deps,
75
96
  )
76
97
 
98
+ if "file://" in found_deps:
99
+ self.install_requires_all_files = True
100
+
77
101
  # ASGI/WSGI Server
78
102
  if "uvicorn" in found_deps:
79
103
  server = PythonServer.Uvicorn
@@ -87,17 +111,30 @@ class PythonProvider:
87
111
  server = None
88
112
  self.server = server
89
113
 
114
+ if "ffmpeg" in found_deps:
115
+ self.uses_ffmpeg = True
116
+ if "pandoc" in found_deps:
117
+ self.uses_pandoc = True
118
+
90
119
  # Set framework
91
120
  if _exists(self.path, "manage.py") and ("django" in found_deps):
92
121
  framework = PythonFramework.Django
93
122
  # Find the settings.py file using glob
94
- settings_file = next(self.path.glob( "**/settings.py"))
123
+ try:
124
+ settings_file = next(self.path.glob("**/settings.py"))
125
+ except StopIteration:
126
+ settings_file = None
95
127
  if settings_file:
96
- asgi_match = re.search(r"ASGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text())
128
+ asgi_match = re.search(
129
+ r"ASGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text()
130
+ )
97
131
  if asgi_match:
98
132
  self.asgi_application = asgi_match.group(1)
99
133
  else:
100
- wsgi_match = re.search(r"WSGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text())
134
+ wsgi_match = re.search(
135
+ r"WSGI_APPLICATION\s*=\s*['\"](.*)['\"]",
136
+ settings_file.read_text(),
137
+ )
101
138
  if wsgi_match:
102
139
  self.wsgi_application = wsgi_match.group(1)
103
140
 
@@ -154,7 +191,7 @@ class PythonProvider:
154
191
  break
155
192
  if not deps:
156
193
  break
157
- return initial_deps-deps
194
+ return initial_deps - deps
158
195
 
159
196
  @classmethod
160
197
  def name(cls) -> str:
@@ -178,7 +215,7 @@ class PythonProvider:
178
215
  return "python"
179
216
 
180
217
  def dependencies(self) -> list[DependencySpec]:
181
- return [
218
+ deps = [
182
219
  DependencySpec(
183
220
  "python",
184
221
  env_var="SHIPIT_PYTHON_VERSION",
@@ -193,73 +230,140 @@ class PythonProvider:
193
230
  use_in_build=True,
194
231
  ),
195
232
  ]
233
+ if self.uses_pandoc:
234
+ deps.append(
235
+ DependencySpec(
236
+ "pandoc",
237
+ env_var="SHIPIT_PANDOC_VERSION",
238
+ use_in_build=False,
239
+ use_in_serve=True,
240
+ )
241
+ )
242
+ if self.uses_ffmpeg:
243
+ deps.append(
244
+ DependencySpec(
245
+ "ffmpeg",
246
+ env_var="SHIPIT_FFMPEG_VERSION",
247
+ use_in_build=False,
248
+ use_in_serve=True,
249
+ )
250
+ )
251
+ return deps
196
252
 
197
253
  def declarations(self) -> Optional[str]:
254
+ if self.only_build:
255
+ return (
256
+ 'cross_platform = getenv("SHIPIT_PYTHON_CROSS_PLATFORM")\n'
257
+ "venv = local_venv\n"
258
+ )
198
259
  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"
260
+ 'cross_platform = getenv("SHIPIT_PYTHON_CROSS_PLATFORM")\n'
261
+ 'python_extra_index_url = getenv("SHIPIT_PYTHON_EXTRA_INDEX_URL")\n'
262
+ 'precompile_python = getenv("SHIPIT_PYTHON_PRECOMPILE") in ["true", "True", "TRUE", "1", "on", "yes", "y", "Y", "YES", "On", "ON"]\n'
263
+ 'python_cross_packages_path = venv["build"] + f"/lib/python{python_version}/site-packages"\n'
264
+ 'python_serve_path = "{}/lib/python{}/site-packages".format(venv["serve"], python_version)\n'
204
265
  )
205
266
 
206
267
  def build_steps(self) -> list[str]:
207
- steps = [
208
- "workdir(app[\"build\"])"
209
- ]
268
+ if not self.only_build:
269
+ steps = ['workdir(app["build"])']
270
+ else:
271
+ steps = []
210
272
 
211
273
  extra_deps = ", ".join([f"{dep}" for dep in self.extra_dependencies])
274
+ has_requirements = _exists(self.path, "requirements.txt")
212
275
  if _exists(self.path, "pyproject.toml"):
213
276
  input_files = ["pyproject.toml"]
214
277
  extra_args = ""
215
278
  if _exists(self.path, "uv.lock"):
216
279
  input_files.append("uv.lock")
217
280
  extra_args = " --locked"
218
- inputs = ", ".join([f"\"{input}\"" for input in input_files])
281
+ inputs = ", ".join([f'"{input}"' for input in input_files])
219
282
  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",
283
+ 'env(UV_PROJECT_ENVIRONMENT=local_venv["build"] if cross_platform else venv["build"])',
284
+ 'copy(".", ".")' if self.install_requires_all_files else None,
285
+ f'run(f"uv sync --compile --python python{{python_version}} --no-managed-python{extra_args}", inputs=[{inputs}], group="install")',
286
+ 'copy("pyproject.toml", "pyproject.toml")'
287
+ if not self.install_requires_all_files
288
+ else None,
289
+ f'run("uv add {extra_deps}", group="install")' if extra_deps else None,
227
290
  ]
228
- if _exists(self.path, "requirements.txt"):
291
+ if not self.only_build:
292
+ steps += [
293
+ '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',
294
+ 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',
295
+ 'run("rm cross-requirements.txt") if cross_platform else None',
296
+ ]
297
+ elif has_requirements or extra_deps:
229
298
  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",
299
+ 'env(UV_PROJECT_ENVIRONMENT=local_venv["build"] if cross_platform else venv["build"])',
300
+ 'run(f"uv init --no-workspace --no-managed-python --python python{python_version}", inputs=[], outputs=["uv.lock"], group="install")',
301
+ 'copy(".", ".")' if self.install_requires_all_files else None,
236
302
  ]
303
+ if has_requirements:
304
+ steps += [
305
+ f'run("uv add -r requirements.txt {extra_deps}", inputs=["requirements.txt"], group="install")',
306
+ ]
307
+ else:
308
+ steps += [
309
+ f'run("uv add {extra_deps}", group="install")',
310
+ ]
311
+ if not self.only_build:
312
+ steps += [
313
+ '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',
314
+ 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',
315
+ 'run("rm cross-requirements.txt") if cross_platform else None',
316
+ ]
237
317
 
238
318
  steps += [
239
- "path((local_venv[\"build\"] if cross_platform else venv[\"build\"]) + \"/bin\")",
240
- "copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
319
+ 'path((local_venv["build"] if cross_platform else venv["build"]) + "/bin")',
320
+ 'copy(".", ".", ignore=[".venv", ".git", "__pycache__"])',
241
321
  ]
242
322
  if self.framework == PythonFramework.MCP:
243
323
  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",
324
+ 'run("mkdir -p {}/bin".format(venv["build"])) if cross_platform else None',
325
+ 'run("cp {}/bin/mcp {}/bin/mcp".format(local_venv["build"], venv["build"])) if cross_platform else None',
246
326
  ]
247
327
  return list(filter(None, steps))
248
328
 
249
329
  def prepare_steps(self) -> Optional[list[str]]:
330
+ if self.only_build:
331
+ return []
250
332
  return [
251
- 'run("echo \\\"Precompiling Python code...\\\"") if precompile_python else None',
333
+ 'run("echo \\"Precompiling Python code...\\"") if precompile_python else None',
252
334
  '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',
335
+ 'run("echo \\"Precompiling package code...\\"") if precompile_python else None',
254
336
  'run("python -m compileall -o 2 {}".format(app["serve"])) if precompile_python else None',
255
337
  ]
256
338
 
339
+ def get_main_file(self) -> Optional[str]:
340
+ paths_to_try = ["main.py", "app.py", "streamlit_app.py", "Home.py", "*_app.py"]
341
+ for path in paths_to_try:
342
+ if "*" in path:
343
+ continue # This is for the glob finder
344
+ if _exists(self.path, path):
345
+ return path
346
+ if _exists(self.path, f"src/{path}"):
347
+ return f"src/{path}"
348
+ for path in paths_to_try:
349
+ try:
350
+ found_path = next(self.path.glob(f"**/{path}.py"))
351
+ except StopIteration:
352
+ found_path = None
353
+ if found_path:
354
+ return found_path.relative_to(self.path)
355
+ return None
356
+
257
357
  def commands(self) -> Dict[str, str]:
358
+ if self.only_build:
359
+ return {}
258
360
  if self.framework == PythonFramework.Django:
259
361
  start_cmd = None
260
362
  if self.server == PythonServer.Daphne and self.asgi_application:
261
363
  asgi_application = format_app_import(self.asgi_application)
262
- start_cmd = f'"python -m daphne {asgi_application} --bind 0.0.0.0 --port 8000"'
364
+ start_cmd = (
365
+ f'"python -m daphne {asgi_application} --bind 0.0.0.0 --port 8000"'
366
+ )
263
367
  elif self.server == PythonServer.Uvicorn:
264
368
  if self.asgi_application:
265
369
  asgi_application = format_app_import(self.asgi_application)
@@ -274,73 +378,58 @@ class PythonProvider:
274
378
  start_cmd = '"python manage.py runserver 0.0.0.0:8000"'
275
379
  migrate_cmd = '"python manage.py migrate"'
276
380
  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
-
381
+
382
+ main_file = self.get_main_file()
383
+
384
+ if not main_file:
385
+ start_cmd = '"python -c \'print(\\"No start command detected, please provide a start command manually\\")\'"'
386
+ return {"start": start_cmd}
387
+
388
+ if self.framework == PythonFramework.FastAPI:
389
+ python_path = file_to_python_path(main_file)
390
+ path = f"{python_path}:app"
283
391
  if self.server == PythonServer.Uvicorn:
284
392
  start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
285
393
  elif self.server == PythonServer.Hypercorn:
286
394
  start_cmd = f'"python -m hypercorn {path} --bind 0.0.0.0:8000"'
287
395
  else:
288
- start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
396
+ start_cmd = '"python -c \'print(\\"No start command detected, please provide a start command manually\\")\'"'
289
397
  return {"start": start_cmd}
398
+
290
399
  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"'
400
+ start_cmd = f'"python -m streamlit run {main_file} --server.port 8000 --server.address 0.0.0.0 --server.headless true"'
401
+
307
402
  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"
403
+ python_path = file_to_python_path(main_file)
404
+ path = f"{python_path}:app"
314
405
  # start_cmd = f'"python -m flask --app {path} run --debug --host 0.0.0.0 --port 8000"'
315
406
  start_cmd = f'"python -m uvicorn {path} --interface=wsgi --host 0.0.0.0 --port 8000"'
407
+
316
408
  elif self.framework == PythonFramework.MCP:
317
- if _exists(self.path, "main.py"):
318
- path = "main.py"
409
+ contents = (self.path / main_file).read_text()
410
+ if 'if __name__ == "__main__"' in contents or "mcp.run" in contents:
411
+ start_cmd = f'"python {main_file}"'
319
412
  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\"])'
413
+ start_cmd = f'"python {{}}/bin/mcp run {main_file} --transport=streamable-http".format(venv["serve"])'
414
+
326
415
  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"
416
+ python_path = file_to_python_path(main_file)
417
+ path = f"{python_path}:app"
331
418
  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"'
419
+
336
420
  else:
337
- start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
421
+ start_cmd = f'"python {main_file}"'
422
+
338
423
  return {"start": start_cmd}
339
424
 
340
425
  def assets(self) -> Optional[Dict[str, str]]:
341
426
  return None
342
427
 
343
428
  def mounts(self) -> list[MountSpec]:
429
+ if self.only_build:
430
+ return [
431
+ MountSpec("local_venv", attach_to_serve=False),
432
+ ]
344
433
  return [
345
434
  MountSpec("app"),
346
435
  MountSpec("venv"),
@@ -348,19 +437,30 @@ class PythonProvider:
348
437
  ]
349
438
 
350
439
  def env(self) -> Optional[Dict[str, str]]:
440
+ if self.only_build:
441
+ return {}
351
442
  # For Django projects, generate an empty env dict to surface the field
352
443
  # in the Shipit file. Other Python projects omit it by default.
353
- env_vars = {
354
- "PYTHONPATH": "python_serve_path",
355
- "HOME": "app[\"serve\"]"
356
- }
444
+ env_vars = {"PYTHONPATH": "python_serve_path", "HOME": 'app["serve"]'}
357
445
  if self.framework == PythonFramework.Streamlit:
358
446
  env_vars["STREAMLIT_SERVER_HEADLESS"] = '"true"'
359
447
  elif self.framework == PythonFramework.MCP:
360
- env_vars["FASTMCP_HOST"] = "\"0.0.0.0\""
361
- env_vars["FASTMCP_PORT"] = "\"8000\""
448
+ env_vars["FASTMCP_HOST"] = '"0.0.0.0"'
449
+ env_vars["FASTMCP_PORT"] = '"8000"'
362
450
  return env_vars
451
+
452
+ def services(self) -> list[ServiceSpec]:
453
+ if self.database == DatabaseType.MySQL:
454
+ return [ServiceSpec(name="database", provider="mysql")]
455
+ elif self.database == DatabaseType.PostgreSQL:
456
+ return [ServiceSpec(name="database", provider="postgres")]
457
+ return []
458
+
363
459
 
364
460
  def format_app_import(asgi_application: str) -> str:
365
461
  # Transform "mysite.asgi.application" to "mysite.asgi:application" using regex
366
462
  return re.sub(r"\.([^.]+)$", r":\1", asgi_application)
463
+
464
+
465
+ def file_to_python_path(path: str) -> str:
466
+ return path.rstrip(".py").replace("/", ".").replace("\\", ".")
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Dict, Optional
5
5
 
6
- from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec, ServiceSpec
7
7
 
8
8
 
9
9
  class StaticFileProvider:
@@ -68,3 +68,6 @@ class StaticFileProvider:
68
68
 
69
69
  def env(self) -> Optional[Dict[str, str]]:
70
70
  return None
71
+
72
+ def services(self) -> list[ServiceSpec]:
73
+ return []
@@ -0,0 +1,5 @@
1
+ __all__ = ["version", "version_info"]
2
+
3
+
4
+ version = "0.6.0"
5
+ version_info = (0, 6, 0, "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