shipit-cli 0.5.1__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shipit/cli.py +104 -13
- shipit/generator.py +10 -0
- shipit/providers/base.py +9 -1
- shipit/providers/gatsby.py +4 -1
- shipit/providers/hugo.py +4 -1
- shipit/providers/laravel.py +4 -1
- shipit/providers/mkdocs.py +16 -41
- shipit/providers/node_static.py +4 -1
- shipit/providers/php.py +4 -1
- shipit/providers/python.py +191 -91
- shipit/providers/staticfile.py +4 -1
- shipit/version.py +2 -2
- {shipit_cli-0.5.1.dist-info → shipit_cli-0.6.0.dist-info}/METADATA +1 -1
- shipit_cli-0.6.0.dist-info/RECORD +19 -0
- shipit_cli-0.5.1.dist-info/RECORD +0 -19
- {shipit_cli-0.5.1.dist-info → shipit_cli-0.6.0.dist-info}/WHEEL +0 -0
- {shipit_cli-0.5.1.dist-info → shipit_cli-0.6.0.dist-info}/entry_points.txt +0 -0
shipit/cli.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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": "
|
|
673
|
-
"3.13": "
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|
shipit/generator.py
CHANGED
|
@@ -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)
|
shipit/providers/base.py
CHANGED
|
@@ -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
|
shipit/providers/gatsby.py
CHANGED
|
@@ -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 []
|
shipit/providers/hugo.py
CHANGED
|
@@ -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 []
|
shipit/providers/laravel.py
CHANGED
|
@@ -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 []
|
shipit/providers/mkdocs.py
CHANGED
|
@@ -3,12 +3,16 @@ 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
|
+
from .staticfile import StaticFileProvider
|
|
8
|
+
from .python import PythonProvider
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
class MkdocsProvider:
|
|
11
|
+
class MkdocsProvider(StaticFileProvider):
|
|
10
12
|
def __init__(self, path: Path):
|
|
11
13
|
self.path = path
|
|
14
|
+
self.python_provider = PythonProvider(path, only_build=True, extra_dependencies={"mkdocs"})
|
|
15
|
+
|
|
12
16
|
@classmethod
|
|
13
17
|
def name(cls) -> str:
|
|
14
18
|
return "mkdocs"
|
|
@@ -30,59 +34,30 @@ class MkdocsProvider:
|
|
|
30
34
|
|
|
31
35
|
def dependencies(self) -> list[DependencySpec]:
|
|
32
36
|
return [
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
),
|
|
37
|
+
*self.python_provider.dependencies(),
|
|
38
|
+
*super().dependencies(),
|
|
51
39
|
]
|
|
52
40
|
|
|
53
41
|
def declarations(self) -> Optional[str]:
|
|
54
|
-
return
|
|
42
|
+
return "mkdocs_version = getenv(\"SHIPIT_MKDOCS_VERSION\") or \"1.6.1\"\n" + (self.python_provider.declarations() or "")
|
|
55
43
|
|
|
56
44
|
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
45
|
return [
|
|
70
|
-
*
|
|
71
|
-
"copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
|
|
46
|
+
*self.python_provider.build_steps(),
|
|
72
47
|
"run(\"uv run mkdocs build --site-dir={}\".format(app[\"build\"]), outputs=[\".\"], group=\"build\")",
|
|
73
48
|
]
|
|
74
49
|
|
|
75
50
|
def prepare_steps(self) -> Optional[list[str]]:
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
def commands(self) -> Dict[str, str]:
|
|
79
|
-
return {"start": '"static-web-server --root /app"'}
|
|
51
|
+
return self.python_provider.prepare_steps()
|
|
80
52
|
|
|
81
53
|
def assets(self) -> Optional[Dict[str, str]]:
|
|
82
54
|
return None
|
|
83
55
|
|
|
84
56
|
def mounts(self) -> list[MountSpec]:
|
|
85
|
-
return [MountSpec("app")]
|
|
57
|
+
return [MountSpec("app"), *self.python_provider.mounts()]
|
|
86
58
|
|
|
87
59
|
def env(self) -> Optional[Dict[str, str]]:
|
|
88
|
-
return
|
|
60
|
+
return self.python_provider.env()
|
|
61
|
+
|
|
62
|
+
def services(self) -> list[ServiceSpec]:
|
|
63
|
+
return []
|
shipit/providers/node_static.py
CHANGED
|
@@ -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 []
|
shipit/providers/php.py
CHANGED
|
@@ -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 []
|
shipit/providers/python.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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"
|
|
281
|
+
inputs = ", ".join([f'"{input}"' for input in input_files])
|
|
219
282
|
steps += [
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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(
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
|
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"] = "
|
|
361
|
-
env_vars["FASTMCP_PORT"] = "
|
|
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("\\", ".")
|
shipit/providers/staticfile.py
CHANGED
|
@@ -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 []
|
shipit/version.py
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
shipit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
shipit/cli.py,sha256=D8SKqygUIhT3w2JoMAvr5St9xBPgqegSZmjzqo6CAzw,52954
|
|
3
|
+
shipit/generator.py,sha256=7kOUUDdibUM8-KRUJJugyVLAdu9UIAtWEe6aU6NVajU,6106
|
|
4
|
+
shipit/version.py,sha256=LMDl7uPF1yrYSfhpy2C7h5pHrS3yfQ6q56kv_YpUm7k,95
|
|
5
|
+
shipit/assets/php/php.ini,sha256=f4irndAjB4GuuouEImRkNV22Q-yw1KqR-43jAMDw730,2531
|
|
6
|
+
shipit/providers/base.py,sha256=WyVazgB1otGiTXOezYmvqPOm1Nt6mYdLvflSiZrXT7g,2593
|
|
7
|
+
shipit/providers/gatsby.py,sha256=2m00AuiuacOgJpDMjp5v7WIG9kC9RGKFBG6-rzwbiW0,2219
|
|
8
|
+
shipit/providers/hugo.py,sha256=YtOmPw4KaE3f8GnPjzLrO3IxlEATCvkc86HOlKDPfMc,1520
|
|
9
|
+
shipit/providers/laravel.py,sha256=1RrwReqDU5OL2NDRKIJEITP3V_M1fC7jndsLN08zW4Y,2858
|
|
10
|
+
shipit/providers/mkdocs.py,sha256=jkBMgkcIyGYBC56m9OzpgaqDy1rdQ0xLpo6nKdqOkZQ,1931
|
|
11
|
+
shipit/providers/node_static.py,sha256=unjswalAp-KX1xnexzpPOyN5Qls00Cvi1MK7lBbYkKQ,2390
|
|
12
|
+
shipit/providers/php.py,sha256=nvB4JHwCxyRbayj3waEHicjATE8C0KxtRnwJuZ7mvFY,2675
|
|
13
|
+
shipit/providers/python.py,sha256=dPKOaxbzUaK1_2LiBpOPZlKfU4dlcac-rWDCew69JKE,18968
|
|
14
|
+
shipit/providers/registry.py,sha256=UisII1dr24ZxmDD8GnpTsyNwPN9W8MnAHQ1Px1iJ-OQ,661
|
|
15
|
+
shipit/providers/staticfile.py,sha256=m1rzbCRO7YWdUH6j0HSMp8OMBktdfwoY_PhFNYe71Xk,1908
|
|
16
|
+
shipit_cli-0.6.0.dist-info/METADATA,sha256=aNn4eZR_TYgETv0PJkb-1TeG3RdUwtZ0MorIHlqfAZk,462
|
|
17
|
+
shipit_cli-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
+
shipit_cli-0.6.0.dist-info/entry_points.txt,sha256=7AE1NjSrHaSDfbfsRRO50KKnHFTbB0Imsccd1WynzAQ,72
|
|
19
|
+
shipit_cli-0.6.0.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
shipit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
shipit/cli.py,sha256=BdkuFvLD3MCTs0nXw3DDOIMkJ4p4eohH3v2jfgFRXeg,49849
|
|
3
|
-
shipit/generator.py,sha256=4pezEo4OzpDFSFqiFpGCpdwo72VkItS490R_f2rIz2k,5755
|
|
4
|
-
shipit/version.py,sha256=hST8ZDPXWcVn83WCgIB61TSxClfCr1MtDM7hisBESS8,95
|
|
5
|
-
shipit/assets/php/php.ini,sha256=f4irndAjB4GuuouEImRkNV22Q-yw1KqR-43jAMDw730,2531
|
|
6
|
-
shipit/providers/base.py,sha256=a_5VA1tV4_QbH83yjPCTHsNR23EJT2CiKUpWA_pu_lo,2373
|
|
7
|
-
shipit/providers/gatsby.py,sha256=uwNjIJloS9JwKXqkbhihgdTTpJL4iL4bLCZ5kuzqqNs,2138
|
|
8
|
-
shipit/providers/hugo.py,sha256=tbn_1t_1AwXHk3-J6mGA2C0D7-3Wpzr-fKpDNhqHy2Q,1439
|
|
9
|
-
shipit/providers/laravel.py,sha256=rDpfx7RyF4sK0xxDAWefX0IiguU2xdgEXP2jJp1Jdzo,2777
|
|
10
|
-
shipit/providers/mkdocs.py,sha256=QJFNt7QWMvYbWeo7WjOgFVKGVcdUnUmUNfFbe4c8ThM,2812
|
|
11
|
-
shipit/providers/node_static.py,sha256=K55BXkNz4QXSXR2wdg5diP6HLpmksC70ph16zox7v6Y,2309
|
|
12
|
-
shipit/providers/php.py,sha256=Hmtv47K5qtYbQ3v9SSk-_KTNlhXedStg2MxhTTOK9ac,2594
|
|
13
|
-
shipit/providers/python.py,sha256=HFH9cFoPvSuJbkVzU0VspFYhOB3X1_IBoi-LodxSHOg,16159
|
|
14
|
-
shipit/providers/registry.py,sha256=UisII1dr24ZxmDD8GnpTsyNwPN9W8MnAHQ1Px1iJ-OQ,661
|
|
15
|
-
shipit/providers/staticfile.py,sha256=Y4oqw6dNDU2crzcWQ5SEgnXHoDy0CXRntABwlgdf1mo,1827
|
|
16
|
-
shipit_cli-0.5.1.dist-info/METADATA,sha256=CCYO1muggGxpLHIW_oS_dod1zGJO89CXCTG0lO3vOrE,462
|
|
17
|
-
shipit_cli-0.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
-
shipit_cli-0.5.1.dist-info/entry_points.txt,sha256=7AE1NjSrHaSDfbfsRRO50KKnHFTbB0Imsccd1WynzAQ,72
|
|
19
|
-
shipit_cli-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|