shipit-cli 0.6.0__py3-none-any.whl → 0.7.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/generator.py CHANGED
@@ -3,7 +3,15 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
  from typing import Dict, List, Optional
5
5
 
6
- from shipit.providers.base import DependencySpec, Provider, ProviderPlan, DetectResult, MountSpec
6
+ from shipit.providers.base import (
7
+ DependencySpec,
8
+ Provider,
9
+ ProviderPlan,
10
+ DetectResult,
11
+ MountSpec,
12
+ VolumeSpec,
13
+ CustomCommands,
14
+ )
7
15
  from shipit.providers.registry import providers as registry_providers
8
16
 
9
17
 
@@ -12,10 +20,10 @@ def _providers() -> list[type[Provider]]:
12
20
  return registry_providers()
13
21
 
14
22
 
15
- def detect_provider(path: Path) -> Provider:
23
+ def detect_provider(path: Path, custom_commands: CustomCommands) -> Provider:
16
24
  matches: list[tuple[type[Provider], DetectResult]] = []
17
25
  for provider_cls in _providers():
18
- res = provider_cls.detect(path)
26
+ res = provider_cls.detect(path, custom_commands)
19
27
  if res:
20
28
  matches.append((provider_cls, res))
21
29
  if not matches:
@@ -69,22 +77,16 @@ def _emit_dependencies_declarations(
69
77
  return "\n".join(lines), serve_vars, build_vars
70
78
 
71
79
 
72
- def _render_assets(assets: Optional[Dict[str, str]]) -> Optional[str]:
73
- if not assets:
74
- return None
75
- inner = ",\n".join([f' "{k}": {v}' for k, v in assets.items()])
76
- return f"{{\n{inner}\n }}"
77
-
78
-
79
- def generate_shipit(path: Path) -> str:
80
- provider_cls = detect_provider(path)
81
- provider = provider_cls(path)
80
+ def generate_shipit(path: Path, custom_commands: CustomCommands) -> str:
81
+ provider_cls = detect_provider(path, custom_commands)
82
+ provider = provider_cls(path, custom_commands)
82
83
 
83
84
  # Collect parts
84
85
  plan = ProviderPlan(
85
86
  serve_name=provider.serve_name(),
86
87
  provider=provider.provider_kind(),
87
88
  mounts=provider.mounts(),
89
+ volumes=provider.volumes(),
88
90
  declarations=provider.declarations(),
89
91
  dependencies=provider.dependencies(),
90
92
  build_steps=provider.build_steps(),
@@ -114,13 +116,17 @@ def generate_shipit(path: Path) -> str:
114
116
  env_lines = "{}"
115
117
  else:
116
118
  env_lines = ",\n".join([f' "{k}": {v}' for k, v in plan.env.items()])
117
- assets_block = _render_assets(plan.assets)
118
119
  mounts_block = None
120
+ volumes_block = None
119
121
  attach_serve_names: list[str] = []
120
122
  if plan.mounts:
121
123
  mounts = list(filter(lambda m: m.attach_to_serve, plan.mounts))
122
124
  attach_serve_names = [m.name for m in mounts]
123
125
  mounts_block = ",\n".join([f" {m.name}" for m in mounts])
126
+ if plan.volumes:
127
+ volumes_block = ",\n".join(
128
+ [f" {v.var_name or v.name}" for v in plan.volumes]
129
+ )
124
130
 
125
131
  out: List[str] = []
126
132
  if dep_block:
@@ -128,6 +134,9 @@ def generate_shipit(path: Path) -> str:
128
134
  out.append("")
129
135
  for m in plan.mounts:
130
136
  out.append(f"{m.name} = mount(\"{m.name}\")")
137
+ if plan.volumes:
138
+ for v in plan.volumes:
139
+ out.append(f"{v.var_name or v.name} = volume(\"{v.name}\", {v.serve_path})")
131
140
  if plan.services:
132
141
  for s in plan.services:
133
142
  out.append(f"{s.name} = service(\n name=\"{s.name}\",\n provider=\"{s.provider}\"\n)")
@@ -144,8 +153,6 @@ def generate_shipit(path: Path) -> str:
144
153
  out.append(" build=[")
145
154
  out.append(build_steps_block)
146
155
  out.append(" ],")
147
- if assets_block:
148
- out.append(" assets=" + assets_block + ",")
149
156
  out.append(f" deps=[{deps_array}],")
150
157
  if plan.prepare:
151
158
  prepare_steps_block = ",\n".join([f" {s}" for s in plan.prepare])
@@ -171,6 +178,10 @@ def generate_shipit(path: Path) -> str:
171
178
  out.append(" mounts=[")
172
179
  out.append(mounts_block)
173
180
  out.append(" ],")
181
+ if volumes_block:
182
+ out.append(" volumes=[")
183
+ out.append(volumes_block)
184
+ out.append(" ],")
174
185
  out.append(")")
175
186
  out.append("")
176
187
  return "\n".join(out)
shipit/procfile.py ADDED
@@ -0,0 +1,106 @@
1
+ # File forked from https://github.com/nickstenning/honcho/blob/main/honcho/environ.py
2
+ # MIT License
3
+ import os
4
+ import re
5
+ from collections import defaultdict, namedtuple
6
+
7
+ PROCFILE_LINE = re.compile(r'^([A-Za-z0-9_-]+):\s*(.+)$')
8
+
9
+
10
+ class Env(object):
11
+
12
+ def __init__(self, config):
13
+ self._c = config
14
+
15
+ @property
16
+ def port(self):
17
+ try:
18
+ return int(self._c['port'])
19
+ except ValueError:
20
+ raise ValueError(f"invalid value for port: '{self._c['port']}'")
21
+
22
+ @property
23
+ def procfile(self):
24
+ return os.path.join(self._c['app_root'], self._c['procfile'])
25
+
26
+ def load_procfile(self):
27
+ with open(self.procfile) as f:
28
+ content = f.read()
29
+
30
+ return parse_procfile(content)
31
+
32
+
33
+ class Procfile(object):
34
+ """A data structure representing a Procfile"""
35
+
36
+ def __init__(self):
37
+ self.processes = {}
38
+
39
+ @classmethod
40
+ def loads(cls, contents):
41
+ p = cls()
42
+ for line in contents.splitlines():
43
+ m = PROCFILE_LINE.match(line)
44
+ if m:
45
+ p.add_process(m.group(1), m.group(2))
46
+ return p
47
+
48
+ def add_process(self, name, command):
49
+ assert name not in self.processes, \
50
+ "process names must be unique within a Procfile"
51
+ self.processes[name] = command
52
+
53
+ def get_start_command(self):
54
+ if "web" in self.processes:
55
+ return self.processes["web"]
56
+ elif "default" in self.processes:
57
+ return self.processes["default"]
58
+ elif "start" in self.processes:
59
+ return self.processes["start"]
60
+ elif len(self.processes) == 1:
61
+ return list(self.processes.values())[0]
62
+ return None
63
+
64
+
65
+ ProcessParams = namedtuple("ProcessParams", "name cmd quiet env")
66
+
67
+
68
+ def expand_processes(processes, concurrency=None, env=None, quiet=None, port=None):
69
+ """
70
+ Get a list of the processes that need to be started given the specified
71
+ list of process types, concurrency, environment, quietness, and base port
72
+ number.
73
+
74
+ Returns a list of ProcessParams objects, which have `name`, `cmd`, `env`,
75
+ and `quiet` attributes, corresponding to the parameters to the constructor
76
+ of `honcho.process.Process`.
77
+ """
78
+ if env is not None and env.get("PORT") is not None:
79
+ port = int(env.get("PORT"))
80
+
81
+ if quiet is None:
82
+ quiet = []
83
+
84
+ con = defaultdict(lambda: 1)
85
+ if concurrency is not None:
86
+ con.update(concurrency)
87
+
88
+ out = []
89
+
90
+ for name, cmd in processes.items():
91
+ for i in range(con[name]):
92
+ n = "{0}.{1}".format(name, i + 1)
93
+ c = cmd
94
+ q = name in quiet
95
+ e = {'SHIPIT_PROCESS_NAME': n}
96
+ if env is not None:
97
+ e.update(env)
98
+ if port is not None:
99
+ e['PORT'] = str(port + i)
100
+
101
+ params = ProcessParams(n, c, q, e)
102
+ out.append(params)
103
+ if port is not None:
104
+ port += 100
105
+
106
+ return out
shipit/providers/base.py CHANGED
@@ -11,12 +11,20 @@ class DetectResult:
11
11
  score: int # Higher score wins when multiple providers match
12
12
 
13
13
 
14
+ @dataclass
15
+ class CustomCommands:
16
+ install: Optional[str] = None
17
+ build: Optional[str] = None
18
+ start: Optional[str] = None
19
+ after_deploy: Optional[str] = None
20
+
21
+
14
22
  class Provider(Protocol):
15
23
  def __init__(self, path: Path): ...
16
24
  @classmethod
17
25
  def name(cls) -> str: ...
18
26
  @classmethod
19
- def detect(cls, path: Path) -> Optional[DetectResult]: ...
27
+ def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]: ...
20
28
  def initialize(self) -> None: ...
21
29
  # Structured plan steps (no path args; use self.path)
22
30
  def serve_name(self) -> str: ...
@@ -27,9 +35,9 @@ class Provider(Protocol):
27
35
  # Prepare: list of Starlark step calls (currently only run(...))
28
36
  def prepare_steps(self) -> Optional[List[str]]: ...
29
37
  def commands(self) -> Dict[str, str]: ...
30
- def assets(self) -> Optional[Dict[str, str]]: ...
31
38
  def services(self) -> List["ServiceSpec"]: ...
32
39
  def mounts(self) -> List["MountSpec"]: ...
40
+ def volumes(self) -> List["VolumeSpec"]: ...
33
41
  def env(self) -> Optional[Dict[str, str]]: ...
34
42
 
35
43
 
@@ -50,6 +58,14 @@ class MountSpec:
50
58
  attach_to_serve: bool = True
51
59
 
52
60
 
61
+ @dataclass
62
+ class VolumeSpec:
63
+ name: str
64
+ # Absolute path inside the serve/runtime environment where the volume is mounted
65
+ serve_path: str
66
+ var_name: Optional[str] = None
67
+
68
+
53
69
  @dataclass
54
70
  class ServiceSpec:
55
71
  name: str
@@ -61,13 +77,13 @@ class ProviderPlan:
61
77
  serve_name: str
62
78
  provider: str
63
79
  mounts: List[MountSpec]
80
+ volumes: List[VolumeSpec] = field(default_factory=list)
64
81
  declarations: Optional[str] = None
65
82
  dependencies: List[DependencySpec] = field(default_factory=list)
66
83
  build_steps: List[str] = field(default_factory=list)
67
84
  prepare: Optional[List[str]] = None
68
85
  services: List[ServiceSpec] = field(default_factory=list)
69
86
  commands: Dict[str, str] = field(default_factory=dict)
70
- assets: Optional[Dict[str, str]] = None
71
87
  env: Optional[Dict[str, str]] = None
72
88
 
73
89
 
@@ -3,18 +3,30 @@ 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, ServiceSpec
6
+ from .base import (
7
+ DetectResult,
8
+ DependencySpec,
9
+ Provider,
10
+ _exists,
11
+ _has_dependency,
12
+ MountSpec,
13
+ ServiceSpec,
14
+ VolumeSpec,
15
+ CustomCommands,
16
+ )
7
17
 
8
18
 
9
19
  class GatsbyProvider:
10
- def __init__(self, path: Path):
20
+ def __init__(self, path: Path, custom_commands: CustomCommands):
11
21
  self.path = path
22
+ self.custom_commands = custom_commands
23
+
12
24
  @classmethod
13
25
  def name(cls) -> str:
14
26
  return "gatsby"
15
27
 
16
28
  @classmethod
17
- def detect(cls, path: Path) -> Optional[DetectResult]:
29
+ def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
18
30
  pkg = path / "package.json"
19
31
  if not pkg.exists():
20
32
  return None
@@ -62,12 +74,12 @@ class GatsbyProvider:
62
74
  def commands(self) -> Dict[str, str]:
63
75
  return {"start": '"static-web-server --root /app"'}
64
76
 
65
- def assets(self) -> Optional[Dict[str, str]]:
66
- return None
67
-
68
77
  def mounts(self) -> list[MountSpec]:
69
78
  return [MountSpec("app")]
70
79
 
80
+ def volumes(self) -> list[VolumeSpec]:
81
+ return []
82
+
71
83
  def env(self) -> Optional[Dict[str, str]]:
72
84
  return None
73
85
 
shipit/providers/hugo.py CHANGED
@@ -3,16 +3,17 @@ 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, ServiceSpec
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, ServiceSpec, VolumeSpec, CustomCommands
7
7
  from .staticfile import StaticFileProvider
8
8
 
9
9
  class HugoProvider(StaticFileProvider):
10
+
10
11
  @classmethod
11
12
  def name(cls) -> str:
12
13
  return "hugo"
13
14
 
14
15
  @classmethod
15
- def detect(cls, path: Path) -> Optional[DetectResult]:
16
+ def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
16
17
  if _exists(path, "hugo.toml", "hugo.json", "hugo.yaml", "hugo.yml"):
17
18
  return DetectResult(cls.name(), 80)
18
19
  if (
@@ -48,3 +49,6 @@ class HugoProvider(StaticFileProvider):
48
49
 
49
50
  def services(self) -> list[ServiceSpec]:
50
51
  return []
52
+
53
+ def volumes(self) -> list[VolumeSpec]:
54
+ return []
@@ -3,18 +3,29 @@ 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, ServiceSpec
6
+ from .base import (
7
+ DetectResult,
8
+ DependencySpec,
9
+ Provider,
10
+ _exists,
11
+ MountSpec,
12
+ ServiceSpec,
13
+ VolumeSpec,
14
+ CustomCommands,
15
+ )
7
16
 
8
17
 
9
18
  class LaravelProvider:
10
- def __init__(self, path: Path):
19
+ def __init__(self, path: Path, custom_commands: CustomCommands):
11
20
  self.path = path
21
+ self.custom_commands = custom_commands
22
+
12
23
  @classmethod
13
24
  def name(cls) -> str:
14
25
  return "laravel"
15
26
 
16
27
  @classmethod
17
- def detect(cls, path: Path) -> Optional[DetectResult]:
28
+ def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
18
29
  if _exists(path, "artisan") and _exists(path, "composer.json"):
19
30
  return DetectResult(cls.name(), 95)
20
31
  return None
@@ -73,12 +84,12 @@ class LaravelProvider:
73
84
  "after_deploy": '"php artisan migrate"',
74
85
  }
75
86
 
76
- def assets(self) -> Optional[Dict[str, str]]:
77
- return None
78
-
79
87
  def mounts(self) -> list[MountSpec]:
80
88
  return [MountSpec("app")]
81
89
 
90
+ def volumes(self) -> list[VolumeSpec]:
91
+ return []
92
+
82
93
  def env(self) -> Optional[Dict[str, str]]:
83
94
  return None
84
95
 
@@ -3,22 +3,31 @@ 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, ServiceSpec
6
+ from .base import (
7
+ DetectResult,
8
+ DependencySpec,
9
+ Provider,
10
+ _exists,
11
+ MountSpec,
12
+ ServiceSpec,
13
+ VolumeSpec,
14
+ CustomCommands,
15
+ )
7
16
  from .staticfile import StaticFileProvider
8
17
  from .python import PythonProvider
9
18
 
10
19
 
11
20
  class MkdocsProvider(StaticFileProvider):
12
- def __init__(self, path: Path):
21
+ def __init__(self, path: Path, custom_commands: CustomCommands):
13
22
  self.path = path
14
- self.python_provider = PythonProvider(path, only_build=True, extra_dependencies={"mkdocs"})
23
+ self.python_provider = PythonProvider(path, custom_commands, only_build=True, extra_dependencies={"mkdocs"})
15
24
 
16
25
  @classmethod
17
26
  def name(cls) -> str:
18
27
  return "mkdocs"
19
28
 
20
29
  @classmethod
21
- def detect(cls, path: Path) -> Optional[DetectResult]:
30
+ def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
22
31
  if _exists(path, "mkdocs.yml", "mkdocs.yaml"):
23
32
  return DetectResult(cls.name(), 85)
24
33
  return None
@@ -50,12 +59,12 @@ class MkdocsProvider(StaticFileProvider):
50
59
  def prepare_steps(self) -> Optional[list[str]]:
51
60
  return self.python_provider.prepare_steps()
52
61
 
53
- def assets(self) -> Optional[Dict[str, str]]:
54
- return None
55
-
56
62
  def mounts(self) -> list[MountSpec]:
57
63
  return [MountSpec("app"), *self.python_provider.mounts()]
58
64
 
65
+ def volumes(self) -> list[VolumeSpec]:
66
+ return []
67
+
59
68
  def env(self) -> Optional[Dict[str, str]]:
60
69
  return self.python_provider.env()
61
70
 
@@ -3,18 +3,30 @@ 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, ServiceSpec
6
+ from .base import (
7
+ DetectResult,
8
+ DependencySpec,
9
+ Provider,
10
+ _exists,
11
+ _has_dependency,
12
+ MountSpec,
13
+ ServiceSpec,
14
+ VolumeSpec,
15
+ CustomCommands,
16
+ )
7
17
 
8
18
 
9
19
  class NodeStaticProvider:
10
- def __init__(self, path: Path):
20
+ def __init__(self, path: Path, custom_commands: CustomCommands):
11
21
  self.path = path
22
+ self.custom_commands = custom_commands
23
+
12
24
  @classmethod
13
25
  def name(cls) -> str:
14
26
  return "node-static"
15
27
 
16
28
  @classmethod
17
- def detect(cls, path: Path) -> Optional[DetectResult]:
29
+ def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
18
30
  pkg = path / "package.json"
19
31
  if not pkg.exists():
20
32
  return None
@@ -63,12 +75,12 @@ class NodeStaticProvider:
63
75
  output_dir = "dist" if (self.path / "dist").exists() else "public"
64
76
  return {"start": f'"static-web-server --root /app/{output_dir}"'}
65
77
 
66
- def assets(self) -> Optional[Dict[str, str]]:
67
- return None
68
-
69
78
  def mounts(self) -> list[MountSpec]:
70
79
  return [MountSpec("app")]
71
80
 
81
+ def volumes(self) -> list[VolumeSpec]:
82
+ return []
83
+
72
84
  def env(self) -> Optional[Dict[str, str]]:
73
85
  return None
74
86
 
shipit/providers/php.py CHANGED
@@ -1,24 +1,38 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from pathlib import Path
4
5
  from typing import Dict, Optional
5
6
 
6
- from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec, ServiceSpec
7
+ from .base import (
8
+ DetectResult,
9
+ DependencySpec,
10
+ Provider,
11
+ _exists,
12
+ MountSpec,
13
+ ServiceSpec,
14
+ VolumeSpec,
15
+ CustomCommands,
16
+ )
7
17
 
8
18
 
9
19
  class PhpProvider:
10
- def __init__(self, path: Path):
20
+ def __init__(self, path: Path, custom_commands: CustomCommands):
11
21
  self.path = path
22
+ self.custom_commands = custom_commands
23
+
12
24
  @classmethod
13
25
  def name(cls) -> str:
14
26
  return "php"
15
27
 
16
28
  @classmethod
17
- def detect(cls, path: Path) -> Optional[DetectResult]:
29
+ def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
18
30
  if _exists(path, "composer.json") and _exists(path, "public/index.php"):
19
31
  return DetectResult(cls.name(), 60)
20
32
  if _exists(path, "index.php") and not _exists(path, "composer.json"):
21
33
  return DetectResult(cls.name(), 10)
34
+ if custom_commands.start and custom_commands.start.startswith("php "):
35
+ return DetectResult(cls.name(), 70)
22
36
  return None
23
37
 
24
38
  def initialize(self) -> None:
@@ -49,12 +63,16 @@ class PhpProvider:
49
63
  return deps
50
64
 
51
65
  def declarations(self) -> Optional[str]:
52
- return "HOME = getenv(\"HOME\")"
66
+ return "HOME = getenv(\"HOME\")\n"
53
67
 
54
68
  def build_steps(self) -> list[str]:
55
69
  steps = [
56
70
  "workdir(app[\"build\"])",
57
71
  ]
72
+ if _exists(self.path, "php.ini"):
73
+ steps.append('copy("php.ini", "{}/php.ini".format(assets["build"]))')
74
+ else:
75
+ steps.append('copy("php/php.ini", "{}/php.ini".format(assets["build"]), base="assets")')
58
76
 
59
77
  if self.has_composer():
60
78
  steps.append("env(HOME=HOME, COMPOSER_FUND=\"0\")")
@@ -67,19 +85,30 @@ class PhpProvider:
67
85
  return None
68
86
 
69
87
  def commands(self) -> Dict[str, str]:
88
+ commands = self.base_commands()
89
+ if self.custom_commands.start:
90
+ commands["start"] = json.dumps(self.custom_commands.start)
91
+ return commands
92
+
93
+ def base_commands(self) -> Dict[str, str]:
70
94
  if _exists(self.path, "public/index.php"):
71
95
  return {"start": '"php -S localhost:8080 -t public"'}
72
96
  elif _exists(self.path, "index.php"):
73
- return {"start": '"php -S localhost:8080" -t .'}
74
-
75
- def assets(self) -> Optional[Dict[str, str]]:
76
- return {"php.ini": "get_asset(\"php/php.ini\")"}
97
+ return {"start": '"php -S localhost:8080 -t ."'}
77
98
 
78
99
  def mounts(self) -> list[MountSpec]:
79
- return [MountSpec("app")]
100
+ return [
101
+ MountSpec("app"),
102
+ MountSpec("assets"),
103
+ ]
104
+
105
+ def volumes(self) -> list[VolumeSpec]:
106
+ return []
80
107
 
81
108
  def env(self) -> Optional[Dict[str, str]]:
82
- return None
109
+ return {
110
+ "PHP_INI_SCAN_DIR": '"{}".format(assets["serve"])',
111
+ }
83
112
 
84
113
  def services(self) -> list[ServiceSpec]:
85
114
  return []