shipit-cli 0.13.4__py3-none-any.whl → 0.15.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/builders/__init__.py +9 -0
- shipit/builders/base.py +14 -0
- shipit/builders/docker.py +250 -0
- shipit/builders/local.py +161 -0
- shipit/cli.py +291 -1323
- shipit/generator.py +56 -36
- shipit/procfile.py +4 -72
- shipit/providers/base.py +50 -11
- shipit/providers/hugo.py +64 -14
- shipit/providers/jekyll.py +123 -0
- shipit/providers/laravel.py +40 -31
- shipit/providers/mkdocs.py +34 -19
- shipit/providers/node_static.py +219 -136
- shipit/providers/php.py +42 -38
- shipit/providers/python.py +284 -228
- shipit/providers/registry.py +2 -2
- shipit/providers/staticfile.py +45 -26
- shipit/providers/wordpress.py +26 -27
- shipit/runners/__init__.py +9 -0
- shipit/runners/base.py +17 -0
- shipit/runners/local.py +105 -0
- shipit/runners/wasmer.py +470 -0
- shipit/shipit_types.py +103 -0
- shipit/ui.py +14 -0
- shipit/utils.py +10 -0
- shipit/version.py +2 -2
- {shipit_cli-0.13.4.dist-info → shipit_cli-0.15.0.dist-info}/METADATA +6 -3
- shipit_cli-0.15.0.dist-info/RECORD +34 -0
- {shipit_cli-0.13.4.dist-info → shipit_cli-0.15.0.dist-info}/WHEEL +1 -1
- shipit_cli-0.13.4.dist-info/RECORD +0 -22
- {shipit_cli-0.13.4.dist-info → shipit_cli-0.15.0.dist-info}/entry_points.txt +0 -0
shipit/providers/python.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
1
|
import re
|
|
5
2
|
from pathlib import Path
|
|
6
3
|
from typing import Dict, Optional, Set
|
|
7
4
|
from enum import Enum
|
|
8
|
-
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from pydantic_settings import SettingsConfigDict
|
|
9
8
|
|
|
10
9
|
from .base import (
|
|
11
10
|
DetectResult,
|
|
@@ -15,7 +14,7 @@ from .base import (
|
|
|
15
14
|
MountSpec,
|
|
16
15
|
ServiceSpec,
|
|
17
16
|
VolumeSpec,
|
|
18
|
-
|
|
17
|
+
Config,
|
|
19
18
|
)
|
|
20
19
|
|
|
21
20
|
|
|
@@ -40,39 +39,61 @@ class DatabaseType(Enum):
|
|
|
40
39
|
PostgreSQL = "postgresql"
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
class
|
|
42
|
+
class PythonConfig(Config):
|
|
43
|
+
model_config = SettingsConfigDict(
|
|
44
|
+
extra="ignore", env_prefix="SHIPIT_"
|
|
45
|
+
)
|
|
46
|
+
|
|
44
47
|
framework: Optional[PythonFramework] = None
|
|
45
48
|
server: Optional[PythonServer] = None
|
|
46
49
|
database: Optional[DatabaseType] = None
|
|
47
|
-
extra_dependencies: Set[str]
|
|
50
|
+
extra_dependencies: Set[str] = Field(default_factory=set)
|
|
48
51
|
asgi_application: Optional[str] = None
|
|
49
52
|
wsgi_application: Optional[str] = None
|
|
50
53
|
uses_ffmpeg: bool = False
|
|
51
54
|
uses_pandoc: bool = False
|
|
52
|
-
only_build: bool = False
|
|
53
55
|
install_requires_all_files: bool = False
|
|
54
|
-
|
|
56
|
+
main_file: Optional[str] = None
|
|
57
|
+
python_version: Optional[str] = "3.13"
|
|
58
|
+
uv_version: Optional[str] = "0.8.15"
|
|
59
|
+
precompile_python: bool = True
|
|
60
|
+
cross_platform: Optional[str] = None
|
|
61
|
+
python_extra_index_url: Optional[str] = None
|
|
62
|
+
pandoc_version: Optional[str] = None
|
|
63
|
+
ffmpeg_version: Optional[str] = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PythonProvider:
|
|
67
|
+
only_build: bool = False
|
|
55
68
|
|
|
56
69
|
def __init__(
|
|
57
70
|
self,
|
|
58
71
|
path: Path,
|
|
59
|
-
|
|
72
|
+
config: PythonConfig,
|
|
60
73
|
only_build: bool = False,
|
|
61
|
-
extra_dependencies: Optional[Set[str]] = None,
|
|
62
74
|
):
|
|
63
75
|
self.path = path
|
|
64
|
-
|
|
65
|
-
python_version = (self.path / ".python-version").read_text().strip()
|
|
66
|
-
else:
|
|
67
|
-
python_version = "3.13"
|
|
68
|
-
self.default_python_version = python_version
|
|
69
|
-
self.extra_dependencies = set()
|
|
76
|
+
self.config = config
|
|
70
77
|
self.only_build = only_build
|
|
71
|
-
self.custom_commands = custom_commands
|
|
72
|
-
self.extra_dependencies = extra_dependencies or set()
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
@classmethod
|
|
80
|
+
def load_config(
|
|
81
|
+
cls,
|
|
82
|
+
path: Path,
|
|
83
|
+
base_config: Config,
|
|
84
|
+
must_have_deps: Optional[Set[str]] = None,
|
|
85
|
+
) -> PythonConfig:
|
|
86
|
+
config = PythonConfig(**base_config.model_dump())
|
|
87
|
+
|
|
88
|
+
if not config.main_file:
|
|
89
|
+
config.main_file = cls.detect_main_file(path)
|
|
90
|
+
|
|
91
|
+
if not config.python_version:
|
|
92
|
+
if _exists(path, ".python-version"):
|
|
93
|
+
python_version = (path / ".python-version").read_text().strip()
|
|
94
|
+
else:
|
|
95
|
+
python_version = "3.13"
|
|
96
|
+
config.python_version = python_version
|
|
76
97
|
|
|
77
98
|
pg_deps = {
|
|
78
99
|
"asyncpg",
|
|
@@ -82,12 +103,21 @@ class PythonProvider:
|
|
|
82
103
|
"psycopg-binary",
|
|
83
104
|
"psycopg2-binary",
|
|
84
105
|
}
|
|
85
|
-
mysql_deps = {
|
|
86
|
-
|
|
106
|
+
mysql_deps = {
|
|
107
|
+
"mysqlclient",
|
|
108
|
+
"pymysql",
|
|
109
|
+
"mysql-connector-python",
|
|
110
|
+
"aiomysql",
|
|
111
|
+
"asyncmy",
|
|
112
|
+
}
|
|
113
|
+
must_have_deps = must_have_deps or set()
|
|
114
|
+
found_deps = cls.check_deps(
|
|
115
|
+
path,
|
|
87
116
|
"file://", # This is not really a dependency, but as a way to check if the install script requires all files
|
|
88
117
|
"streamlit",
|
|
89
118
|
"django",
|
|
90
119
|
"mcp",
|
|
120
|
+
"mcp[cli]",
|
|
91
121
|
"fastapi",
|
|
92
122
|
"flask",
|
|
93
123
|
"python-fasthtml",
|
|
@@ -100,100 +130,135 @@ class PythonProvider:
|
|
|
100
130
|
# "gunicorn",
|
|
101
131
|
*mysql_deps,
|
|
102
132
|
*pg_deps,
|
|
133
|
+
*must_have_deps,
|
|
103
134
|
)
|
|
104
135
|
|
|
105
136
|
if "file://" in found_deps:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
config.install_requires_all_files = True
|
|
138
|
+
|
|
139
|
+
if not config.server:
|
|
140
|
+
# ASGI/WSGI Server
|
|
141
|
+
if "uvicorn" in found_deps:
|
|
142
|
+
server = PythonServer.Uvicorn
|
|
143
|
+
elif "hypercorn" in found_deps:
|
|
144
|
+
server = PythonServer.Hypercorn
|
|
145
|
+
# elif "gunicorn" in found_deps:
|
|
146
|
+
# server = PythonServer.Gunicorn
|
|
147
|
+
elif "daphne" in found_deps:
|
|
148
|
+
server = PythonServer.Daphne
|
|
149
|
+
else:
|
|
150
|
+
server = None
|
|
151
|
+
config.server = server
|
|
120
152
|
|
|
121
153
|
if "ffmpeg" in found_deps:
|
|
122
|
-
|
|
154
|
+
config.uses_ffmpeg = True
|
|
123
155
|
if "pandoc" in found_deps:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
config.uses_pandoc = True
|
|
157
|
+
|
|
158
|
+
if not config.framework:
|
|
159
|
+
# Set framework
|
|
160
|
+
if _exists(path, "manage.py") and ("django" in found_deps):
|
|
161
|
+
framework = PythonFramework.Django
|
|
162
|
+
elif "streamlit" in found_deps:
|
|
163
|
+
framework = PythonFramework.Streamlit
|
|
164
|
+
elif "mcp" in found_deps:
|
|
165
|
+
framework = PythonFramework.MCP
|
|
166
|
+
elif "fastapi" in found_deps:
|
|
167
|
+
framework = PythonFramework.FastAPI
|
|
168
|
+
elif "flask" in found_deps:
|
|
169
|
+
framework = PythonFramework.Flask
|
|
170
|
+
elif "python-fasthtml" in found_deps:
|
|
171
|
+
framework = PythonFramework.FastHTML
|
|
172
|
+
else:
|
|
173
|
+
framework = None
|
|
174
|
+
config.framework = framework
|
|
175
|
+
|
|
176
|
+
if not config.server and config.framework:
|
|
177
|
+
if config.framework == PythonFramework.Django:
|
|
178
|
+
config.server = PythonServer.Uvicorn
|
|
179
|
+
elif config.framework == PythonFramework.FastAPI:
|
|
180
|
+
config.server = PythonServer.Uvicorn
|
|
181
|
+
elif config.framework == PythonFramework.Flask:
|
|
182
|
+
config.server = PythonServer.Uvicorn
|
|
183
|
+
elif config.framework == PythonFramework.FastHTML:
|
|
184
|
+
config.server = PythonServer.Uvicorn
|
|
185
|
+
|
|
186
|
+
if config.server == PythonServer.Uvicorn:
|
|
187
|
+
must_have_deps.add("uvicorn")
|
|
188
|
+
|
|
189
|
+
if not config.asgi_application and not config.wsgi_application:
|
|
190
|
+
if config.framework == PythonFramework.Django:
|
|
191
|
+
# Find the settings.py file using glob
|
|
192
|
+
try:
|
|
193
|
+
settings_file = next(path.glob("**/settings.py"))
|
|
194
|
+
except StopIteration:
|
|
195
|
+
settings_file = None
|
|
196
|
+
if settings_file:
|
|
197
|
+
asgi_match = re.search(
|
|
198
|
+
r"ASGI_APPLICATION\s*=\s*['\"](.*)['\"]",
|
|
148
199
|
settings_file.read_text(),
|
|
149
200
|
)
|
|
150
|
-
if
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
framework
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
201
|
+
if asgi_match:
|
|
202
|
+
config.asgi_application = format_app_import(
|
|
203
|
+
asgi_match.group(1)
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
wsgi_match = re.search(
|
|
207
|
+
r"WSGI_APPLICATION\s*=\s*['\"](.*)['\"]",
|
|
208
|
+
settings_file.read_text(),
|
|
209
|
+
)
|
|
210
|
+
if wsgi_match:
|
|
211
|
+
config.wsgi_application = format_app_import(
|
|
212
|
+
wsgi_match.group(1)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
python_path = file_to_python_path(config.main_file)
|
|
216
|
+
if config.framework == PythonFramework.FastAPI:
|
|
217
|
+
config.asgi_application = python_path
|
|
218
|
+
elif config.framework == PythonFramework.Flask:
|
|
219
|
+
config.wsgi_application = python_path
|
|
220
|
+
elif config.framework == PythonFramework.MCP:
|
|
221
|
+
config.asgi_application = python_path
|
|
222
|
+
elif config.framework == PythonFramework.FastHTML:
|
|
223
|
+
config.asgi_application = python_path
|
|
224
|
+
|
|
225
|
+
is_uvicorn_start = config.commands.start and config.commands.start.startswith(
|
|
226
|
+
"uvicorn "
|
|
227
|
+
)
|
|
228
|
+
framework_should_use_uvicorn = config.framework in [
|
|
229
|
+
PythonFramework.Django,
|
|
230
|
+
PythonFramework.FastAPI,
|
|
231
|
+
PythonFramework.Flask,
|
|
232
|
+
]
|
|
233
|
+
if is_uvicorn_start or (framework_should_use_uvicorn and not config.server):
|
|
234
|
+
must_have_deps.add("uvicorn")
|
|
235
|
+
config.server = PythonServer.Uvicorn
|
|
236
|
+
if config.framework == PythonFramework.MCP:
|
|
237
|
+
must_have_deps.add("mcp[cli]")
|
|
238
|
+
|
|
239
|
+
for dep in must_have_deps:
|
|
240
|
+
if dep not in found_deps:
|
|
241
|
+
config.extra_dependencies.add(dep)
|
|
242
|
+
|
|
243
|
+
if not config.database:
|
|
244
|
+
# Database
|
|
245
|
+
if mysql_deps & found_deps:
|
|
246
|
+
database = DatabaseType.MySQL
|
|
247
|
+
elif pg_deps & found_deps:
|
|
248
|
+
database = DatabaseType.PostgreSQL
|
|
249
|
+
else:
|
|
250
|
+
database = None
|
|
251
|
+
config.database = database
|
|
252
|
+
|
|
253
|
+
return config
|
|
190
254
|
|
|
191
|
-
|
|
255
|
+
@classmethod
|
|
256
|
+
def check_deps(cls, path: Path, *deps: str) -> Set[str]:
|
|
192
257
|
deps = set([dep.lower() for dep in deps])
|
|
193
258
|
initial_deps = set(deps)
|
|
194
259
|
for file in ["requirements.txt", "pyproject.toml"]:
|
|
195
|
-
if _exists(
|
|
196
|
-
for line in (
|
|
260
|
+
if _exists(path, file):
|
|
261
|
+
for line in (path / file).read_text().splitlines():
|
|
197
262
|
for dep in set(deps):
|
|
198
263
|
if dep in line.lower():
|
|
199
264
|
deps.remove(dep)
|
|
@@ -210,57 +275,56 @@ class PythonProvider:
|
|
|
210
275
|
return "python"
|
|
211
276
|
|
|
212
277
|
@classmethod
|
|
213
|
-
def detect(
|
|
278
|
+
def detect(
|
|
279
|
+
cls, path: Path, config: Config
|
|
280
|
+
) -> Optional[DetectResult]:
|
|
214
281
|
if _exists(path, "pyproject.toml", "requirements.txt"):
|
|
215
282
|
if _exists(path, "manage.py"):
|
|
216
283
|
return DetectResult(cls.name(), 70)
|
|
217
284
|
return DetectResult(cls.name(), 50)
|
|
218
|
-
if
|
|
219
|
-
if
|
|
285
|
+
if config.commands.start:
|
|
286
|
+
if (
|
|
287
|
+
config.commands.start.startswith("python ")
|
|
288
|
+
or config.commands.start.startswith("uv ")
|
|
289
|
+
or config.commands.start.startswith("uvicorn ")
|
|
290
|
+
or config.commands.start.startswith("gunicorn ")
|
|
291
|
+
):
|
|
220
292
|
return DetectResult(cls.name(), 80)
|
|
221
293
|
if cls.detect_main_file(path):
|
|
222
294
|
return DetectResult(cls.name(), 10)
|
|
223
295
|
return None
|
|
224
296
|
|
|
225
|
-
def
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def serve_name(self) -> str:
|
|
229
|
-
return self.path.name
|
|
230
|
-
|
|
231
|
-
def platform(self) -> Optional[str]:
|
|
232
|
-
return self.framework.value if self.framework else None
|
|
297
|
+
def serve_name(self) -> Optional[str]:
|
|
298
|
+
return None
|
|
233
299
|
|
|
234
300
|
def dependencies(self) -> list[DependencySpec]:
|
|
235
301
|
deps = [
|
|
236
302
|
DependencySpec(
|
|
237
303
|
"python",
|
|
238
|
-
|
|
239
|
-
default_version=self.default_python_version,
|
|
304
|
+
var_name="config.python_version",
|
|
240
305
|
use_in_build=True,
|
|
241
306
|
use_in_serve=True,
|
|
242
307
|
),
|
|
243
308
|
DependencySpec(
|
|
244
309
|
"uv",
|
|
245
|
-
|
|
246
|
-
default_version="0.8.15",
|
|
310
|
+
var_name="config.uv_version",
|
|
247
311
|
use_in_build=True,
|
|
248
312
|
),
|
|
249
313
|
]
|
|
250
|
-
if self.uses_pandoc:
|
|
314
|
+
if self.config.uses_pandoc:
|
|
251
315
|
deps.append(
|
|
252
316
|
DependencySpec(
|
|
253
317
|
"pandoc",
|
|
254
|
-
|
|
318
|
+
var_name="config.pandoc_version",
|
|
255
319
|
use_in_build=False,
|
|
256
320
|
use_in_serve=True,
|
|
257
321
|
)
|
|
258
322
|
)
|
|
259
|
-
if self.uses_ffmpeg:
|
|
323
|
+
if self.config.uses_ffmpeg:
|
|
260
324
|
deps.append(
|
|
261
325
|
DependencySpec(
|
|
262
326
|
"ffmpeg",
|
|
263
|
-
|
|
327
|
+
var_name="config.ffmpeg_version",
|
|
264
328
|
use_in_build=False,
|
|
265
329
|
use_in_serve=True,
|
|
266
330
|
)
|
|
@@ -270,25 +334,27 @@ class PythonProvider:
|
|
|
270
334
|
def declarations(self) -> Optional[str]:
|
|
271
335
|
if self.only_build:
|
|
272
336
|
return (
|
|
273
|
-
|
|
337
|
+
"python_version = config.python_version\n"
|
|
338
|
+
"cross_platform = config.cross_platform\n"
|
|
274
339
|
"venv = local_venv\n"
|
|
275
340
|
)
|
|
276
341
|
return (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
'
|
|
282
|
-
'
|
|
342
|
+
"python_version = config.python_version\n"
|
|
343
|
+
"cross_platform = config.cross_platform\n"
|
|
344
|
+
"python_extra_index_url = config.python_extra_index_url\n"
|
|
345
|
+
"precompile_python = config.precompile_python\n"
|
|
346
|
+
'python_cross_packages_path = venv.path + f"/lib/python{python_version}/site-packages"\n'
|
|
347
|
+
'python_serve_site_packages_path = "{}/lib/python{}/site-packages".format(venv.serve_path, python_version)\n'
|
|
348
|
+
'app_serve_path = app.serve_path\n'
|
|
283
349
|
)
|
|
284
350
|
|
|
285
351
|
def build_steps(self) -> list[str]:
|
|
286
352
|
if not self.only_build:
|
|
287
|
-
steps = ['workdir(app
|
|
353
|
+
steps = ['workdir(app.path)']
|
|
288
354
|
else:
|
|
289
|
-
steps = ['workdir(temp
|
|
355
|
+
steps = ['workdir(temp.path)']
|
|
290
356
|
|
|
291
|
-
extra_deps = ", ".join([f"{dep}" for dep in self.extra_dependencies])
|
|
357
|
+
extra_deps = ", ".join([f"{dep}" for dep in self.config.extra_dependencies])
|
|
292
358
|
has_requirements = _exists(self.path, "requirements.txt")
|
|
293
359
|
if _exists(self.path, "pyproject.toml"):
|
|
294
360
|
input_files = ["pyproject.toml"]
|
|
@@ -309,11 +375,11 @@ class PythonProvider:
|
|
|
309
375
|
# Join inputs
|
|
310
376
|
inputs = ", ".join([f'"{input}"' for input in input_files])
|
|
311
377
|
steps += [
|
|
312
|
-
'env(UV_PROJECT_ENVIRONMENT=local_venv
|
|
313
|
-
'copy(".", ".")' if self.install_requires_all_files else None,
|
|
378
|
+
'env(UV_PROJECT_ENVIRONMENT=local_venv.path if cross_platform else venv.path, UV_PYTHON_PREFERENCE="only-system", UV_PYTHON=f"python{python_version}")',
|
|
379
|
+
'copy(".", ".")' if self.config.install_requires_all_files else None,
|
|
314
380
|
f'run(f"uv sync{extra_args}", inputs=[{inputs}], group="install")',
|
|
315
381
|
'copy("pyproject.toml", "pyproject.toml")'
|
|
316
|
-
if not self.install_requires_all_files
|
|
382
|
+
if not self.config.install_requires_all_files
|
|
317
383
|
else None,
|
|
318
384
|
f'run("uv add {extra_deps}", group="install")' if extra_deps else None,
|
|
319
385
|
]
|
|
@@ -325,9 +391,11 @@ class PythonProvider:
|
|
|
325
391
|
]
|
|
326
392
|
elif has_requirements or extra_deps:
|
|
327
393
|
steps += [
|
|
328
|
-
'env(UV_PROJECT_ENVIRONMENT=local_venv
|
|
394
|
+
'env(UV_PROJECT_ENVIRONMENT=local_venv.path if cross_platform else venv.path)',
|
|
329
395
|
'run(f"uv init", inputs=[], outputs=["uv.lock"], group="install")',
|
|
330
|
-
'copy(".", ".", ignore=[".venv", ".git", "__pycache__"])'
|
|
396
|
+
'copy(".", ".", ignore=[".venv", ".git", "__pycache__"])'
|
|
397
|
+
if self.config.install_requires_all_files
|
|
398
|
+
else None,
|
|
331
399
|
]
|
|
332
400
|
if has_requirements:
|
|
333
401
|
steps += [
|
|
@@ -345,15 +413,17 @@ class PythonProvider:
|
|
|
345
413
|
]
|
|
346
414
|
|
|
347
415
|
steps += [
|
|
348
|
-
'path((local_venv
|
|
349
|
-
'copy(".", ".", ignore=[".venv", ".git", "__pycache__"])'
|
|
416
|
+
'path((local_venv.path if cross_platform else venv.path) + "/bin")',
|
|
417
|
+
'copy(".", ".", ignore=[".venv", ".git", "__pycache__"])'
|
|
418
|
+
if not self.config.install_requires_all_files
|
|
419
|
+
else None,
|
|
350
420
|
]
|
|
351
|
-
if self.framework == PythonFramework.MCP:
|
|
421
|
+
if self.config.framework == PythonFramework.MCP:
|
|
352
422
|
steps += [
|
|
353
|
-
'run("mkdir -p {}/bin".format(venv
|
|
354
|
-
'run("cp {}/bin/mcp {}/bin/mcp".format(local_venv
|
|
423
|
+
'run("mkdir -p {}/bin".format(venv.path)) if cross_platform else None',
|
|
424
|
+
'run("cp {}/bin/mcp {}/bin/mcp".format(local_venv.path, venv.path)) if cross_platform else None',
|
|
355
425
|
]
|
|
356
|
-
if self.framework == PythonFramework.Django:
|
|
426
|
+
if self.config.framework == PythonFramework.Django:
|
|
357
427
|
steps += [
|
|
358
428
|
'run("python manage.py collectstatic --noinput", group="build")',
|
|
359
429
|
]
|
|
@@ -364,9 +434,9 @@ class PythonProvider:
|
|
|
364
434
|
return []
|
|
365
435
|
return [
|
|
366
436
|
'run("echo \\"Precompiling Python code...\\"") if precompile_python else None',
|
|
367
|
-
'run(f"python -m compileall -o 2 {python_serve_site_packages_path}") if precompile_python else None',
|
|
437
|
+
'run(f"python -m compileall -o 2 {python_serve_site_packages_path} || true") if precompile_python else None',
|
|
368
438
|
'run("echo \\"Precompiling package code...\\"") if precompile_python else None',
|
|
369
|
-
'run(f"python -m compileall -o 2 {app_serve_path}") if precompile_python else None',
|
|
439
|
+
'run(f"python -m compileall -o 2 {app_serve_path} || true") if precompile_python else None',
|
|
370
440
|
]
|
|
371
441
|
|
|
372
442
|
@classmethod
|
|
@@ -380,91 +450,74 @@ class PythonProvider:
|
|
|
380
450
|
if _exists(root_path, f"src/{path}"):
|
|
381
451
|
return f"src/{path}"
|
|
382
452
|
for path in paths_to_try:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
found_path = None
|
|
453
|
+
found_path = next(root_path.glob(f"*/{path}"), None)
|
|
454
|
+
if not found_path:
|
|
455
|
+
found_path = next(root_path.glob(f"*/*/{path}"), None)
|
|
387
456
|
if found_path:
|
|
388
457
|
return str(found_path.relative_to(root_path))
|
|
389
458
|
return None
|
|
390
459
|
|
|
391
|
-
@cached_property
|
|
392
|
-
def main_file(self) -> Optional[str]:
|
|
393
|
-
return self.detect_main_file(self.path)
|
|
394
|
-
|
|
395
460
|
def commands(self) -> Dict[str, str]:
|
|
396
|
-
|
|
397
|
-
if self.custom_commands.start:
|
|
398
|
-
commands["start"] = json.dumps(self.custom_commands.start)
|
|
399
|
-
return commands
|
|
461
|
+
return self.base_commands()
|
|
400
462
|
|
|
401
463
|
def base_commands(self) -> Dict[str, str]:
|
|
402
464
|
if self.only_build:
|
|
403
465
|
return {}
|
|
404
|
-
if self.framework == PythonFramework.Django:
|
|
405
|
-
start_cmd = None
|
|
406
|
-
if self.server == PythonServer.Daphne and self.asgi_application:
|
|
407
|
-
asgi_application = format_app_import(self.asgi_application)
|
|
408
|
-
start_cmd = (
|
|
409
|
-
f'f"python -m daphne {asgi_application} --bind 0.0.0.0 --port {{PORT}}"'
|
|
410
|
-
)
|
|
411
|
-
elif self.server == PythonServer.Uvicorn:
|
|
412
|
-
if self.asgi_application:
|
|
413
|
-
asgi_application = format_app_import(self.asgi_application)
|
|
414
|
-
start_cmd = f'f"python -m uvicorn {asgi_application} --host 0.0.0.0 --port {{PORT}}"'
|
|
415
|
-
elif self.wsgi_application:
|
|
416
|
-
wsgi_application = format_app_import(self.wsgi_application)
|
|
417
|
-
start_cmd = f'f"python -m uvicorn {wsgi_application} --interface=wsgi --host 0.0.0.0 --port {{PORT}}"'
|
|
418
|
-
# elif self.server == PythonServer.Gunicorn:
|
|
419
|
-
# start_cmd = f'"fpython -m gunicorn {self.wsgi_application} --bind 0.0.0.0 --port {{PORT}}"'
|
|
420
|
-
if not start_cmd:
|
|
421
|
-
# We run the default runserver command if no server is specified
|
|
422
|
-
start_cmd = 'f"python manage.py runserver 0.0.0.0:{PORT}"'
|
|
423
|
-
migrate_cmd = '"python manage.py migrate"'
|
|
424
|
-
return {"start": start_cmd, "after_deploy": migrate_cmd}
|
|
425
|
-
|
|
426
|
-
main_file = self.main_file
|
|
427
|
-
|
|
428
|
-
if not main_file:
|
|
429
|
-
start_cmd = '"python -c \'print(\\"No start command detected, please provide a start command manually\\")\'"'
|
|
430
|
-
return {"start": start_cmd}
|
|
431
|
-
|
|
432
|
-
if self.framework == PythonFramework.FastAPI:
|
|
433
|
-
python_path = file_to_python_path(main_file)
|
|
434
|
-
path = f"{python_path}:app"
|
|
435
|
-
if self.server == PythonServer.Uvicorn:
|
|
436
|
-
start_cmd = f'f"python -m uvicorn {path} --host 0.0.0.0 --port {{PORT}}"'
|
|
437
|
-
elif self.server == PythonServer.Hypercorn:
|
|
438
|
-
start_cmd = f'f"python -m hypercorn {path} --bind 0.0.0.0:{{PORT}}"'
|
|
439
|
-
else:
|
|
440
|
-
start_cmd = '"python -c \'print(\\"No start command detected, please provide a start command manually\\")\'"'
|
|
441
|
-
return {"start": start_cmd}
|
|
442
|
-
|
|
443
|
-
elif self.framework == PythonFramework.Streamlit:
|
|
444
|
-
start_cmd = f'f"python -m streamlit run {main_file} --server.port {{PORT}} --server.address 0.0.0.0 --server.headless true"'
|
|
445
466
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
elif self.
|
|
467
|
+
start_cmd = None
|
|
468
|
+
if self.config.server == PythonServer.Daphne:
|
|
469
|
+
assert self.config.asgi_application, (
|
|
470
|
+
"No ASGI application found for Daphne"
|
|
471
|
+
)
|
|
472
|
+
start_cmd = f'f"daphne {self.config.asgi_application} --bind 0.0.0.0 --port {{PORT}}"'
|
|
473
|
+
# elif self.config.server == PythonServer.Gunicorn:
|
|
474
|
+
# assert self.config.wsgi_application, "No WSGI application found"
|
|
475
|
+
# start_cmd = f'f"gunicorn {self.config.wsgi_application} --bind 0.0.0.0 --port {{PORT}}"'
|
|
476
|
+
elif self.config.server == PythonServer.Uvicorn:
|
|
477
|
+
if not self.config.main_file:
|
|
478
|
+
assert (
|
|
479
|
+
self.config.asgi_application or self.config.wsgi_application
|
|
480
|
+
), (
|
|
481
|
+
"No ASGI or WSGI application found for Uvicorn and no main file found"
|
|
482
|
+
)
|
|
483
|
+
if self.config.asgi_application:
|
|
484
|
+
start_cmd = f'f"uvicorn {self.config.asgi_application} --host 0.0.0.0 --port {{PORT}}"'
|
|
485
|
+
elif self.config.wsgi_application:
|
|
486
|
+
start_cmd = f'f"uvicorn {self.config.wsgi_application} --interface=wsgi --host 0.0.0.0 --port {{PORT}}"'
|
|
487
|
+
elif self.config.server == PythonServer.Hypercorn:
|
|
488
|
+
assert self.config.asgi_application, (
|
|
489
|
+
"No ASGI application found for Hypercorn"
|
|
490
|
+
)
|
|
491
|
+
start_cmd = (
|
|
492
|
+
f'f"hypercorn {self.config.asgi_application} --bind 0.0.0.0:{{PORT}}"'
|
|
493
|
+
)
|
|
494
|
+
elif self.config.framework == PythonFramework.Streamlit:
|
|
495
|
+
assert self.config.main_file, "No main file found for Streamlit"
|
|
496
|
+
main_file = self.config.main_file
|
|
497
|
+
start_cmd = f'f"streamlit run {main_file} --server.port {{PORT}} --server.address 0.0.0.0 --server.headless true"'
|
|
498
|
+
elif self.config.framework == PythonFramework.MCP:
|
|
499
|
+
main_file = self.config.main_file
|
|
500
|
+
assert main_file, "No main file found for MCP"
|
|
453
501
|
contents = (self.path / main_file).read_text()
|
|
454
502
|
if 'if __name__ == "__main__"' in contents or "mcp.run" in contents:
|
|
455
503
|
start_cmd = f'"python {main_file}"'
|
|
456
504
|
else:
|
|
457
|
-
start_cmd = f'"python {{}}/bin/mcp run {main_file} --transport=streamable-http".format(venv
|
|
505
|
+
start_cmd = f'"python {{}}/bin/mcp run {main_file} --transport=streamable-http".format(venv.serve_path)'
|
|
458
506
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
start_cmd = f'f"python -m uvicorn {path} --host 0.0.0.0 --port {{PORT}}"'
|
|
507
|
+
if not start_cmd:
|
|
508
|
+
if self.config.main_file:
|
|
509
|
+
start_cmd = f'"python {self.config.main_file}"'
|
|
463
510
|
|
|
464
|
-
|
|
465
|
-
|
|
511
|
+
if self.config.framework == PythonFramework.Django:
|
|
512
|
+
if not start_cmd:
|
|
513
|
+
start_cmd = 'f"python manage.py runserver 0.0.0.0:{PORT}"'
|
|
514
|
+
migrate_cmd = '"python manage.py migrate"'
|
|
515
|
+
return {"start": start_cmd, "after_deploy": migrate_cmd}
|
|
466
516
|
|
|
467
|
-
|
|
517
|
+
if start_cmd:
|
|
518
|
+
return {"start": start_cmd}
|
|
519
|
+
else:
|
|
520
|
+
return {}
|
|
468
521
|
|
|
469
522
|
def mounts(self) -> list[MountSpec]:
|
|
470
523
|
if self.only_build:
|
|
@@ -486,24 +539,24 @@ class PythonProvider:
|
|
|
486
539
|
return {}
|
|
487
540
|
# For Django projects, generate an empty env dict to surface the field
|
|
488
541
|
# in the Shipit file. Other Python projects omit it by default.
|
|
489
|
-
python_path =
|
|
490
|
-
main_file = self.main_file
|
|
542
|
+
python_path = 'f"{app_serve_path}:{python_serve_site_packages_path}"'
|
|
543
|
+
main_file = self.config.main_file
|
|
491
544
|
if main_file and main_file.startswith("src/"):
|
|
492
|
-
python_path =
|
|
545
|
+
python_path = 'f"{app_serve_path}:{app_serve_path}/src:{python_serve_site_packages_path}"'
|
|
493
546
|
else:
|
|
494
|
-
python_path =
|
|
495
|
-
env_vars = {"PYTHONPATH": python_path, "HOME": 'app
|
|
496
|
-
if self.framework == PythonFramework.Streamlit:
|
|
547
|
+
python_path = 'f"{app_serve_path}:{python_serve_site_packages_path}"'
|
|
548
|
+
env_vars = {"PYTHONPATH": python_path, "HOME": 'app.serve_path'}
|
|
549
|
+
if self.config.framework == PythonFramework.Streamlit:
|
|
497
550
|
env_vars["STREAMLIT_SERVER_HEADLESS"] = '"true"'
|
|
498
|
-
elif self.framework == PythonFramework.MCP:
|
|
551
|
+
elif self.config.framework == PythonFramework.MCP:
|
|
499
552
|
env_vars["FASTMCP_HOST"] = '"0.0.0.0"'
|
|
500
|
-
env_vars["FASTMCP_PORT"] =
|
|
553
|
+
env_vars["FASTMCP_PORT"] = "PORT"
|
|
501
554
|
return env_vars
|
|
502
|
-
|
|
555
|
+
|
|
503
556
|
def services(self) -> list[ServiceSpec]:
|
|
504
|
-
if self.database == DatabaseType.MySQL:
|
|
557
|
+
if self.config.database == DatabaseType.MySQL:
|
|
505
558
|
return [ServiceSpec(name="database", provider="mysql")]
|
|
506
|
-
elif self.database == DatabaseType.PostgreSQL:
|
|
559
|
+
elif self.config.database == DatabaseType.PostgreSQL:
|
|
507
560
|
return [ServiceSpec(name="database", provider="postgres")]
|
|
508
561
|
return []
|
|
509
562
|
|
|
@@ -513,5 +566,8 @@ def format_app_import(asgi_application: str) -> str:
|
|
|
513
566
|
return re.sub(r"\.([^.]+)$", r":\1", asgi_application)
|
|
514
567
|
|
|
515
568
|
|
|
516
|
-
def file_to_python_path(path: str) -> str:
|
|
517
|
-
|
|
569
|
+
def file_to_python_path(path: Optional[str]) -> Optional[str]:
|
|
570
|
+
if not path:
|
|
571
|
+
return None
|
|
572
|
+
file = path.rstrip(".py").replace("/", ".").replace("\\", ".")
|
|
573
|
+
return f"{file}:app"
|