shipit-cli 0.1.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 ADDED
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, List, Optional
5
+
6
+ from shipit.providers.base import DependencySpec, Provider, ProviderPlan, DetectResult
7
+ from shipit.providers.registry import providers as registry_providers
8
+
9
+
10
+ def _providers() -> list[Provider]:
11
+ # Load providers from modular registry
12
+ return registry_providers()
13
+
14
+
15
+ def detect_provider(path: Path) -> Provider:
16
+ matches: list[tuple[Provider, DetectResult]] = []
17
+ for p in _providers():
18
+ res = p.detect(path)
19
+ if res:
20
+ matches.append((p, res))
21
+ if not matches:
22
+ # Default to static site as the safest fallback
23
+ from shipit.providers.staticfile import StaticFileProvider
24
+
25
+ return StaticFileProvider()
26
+ # Highest score wins; tie-breaker by order
27
+ matches.sort(key=lambda x: x[1].score, reverse=True)
28
+ return matches[0][0]
29
+
30
+
31
+ def _sanitize_alias(name: str) -> str:
32
+ # Keep it predictable and valid in Starlark: letters, numbers, underscore
33
+ # Remove dashes to keep prior style (e.g., staticwebserver)
34
+ allowed = [c if c.isalnum() or c == "_" else "" for c in name]
35
+ alias = "".join(allowed)
36
+ return alias.replace("-", "")
37
+
38
+
39
+ def _emit_dependencies_declarations(
40
+ deps: List[DependencySpec],
41
+ ) -> tuple[str, List[str], List[str]]:
42
+ lines: List[str] = []
43
+ declared: set[str] = set()
44
+ serve_vars: List[str] = []
45
+ build_vars: List[str] = []
46
+
47
+ for dep in deps:
48
+ alias = dep.alias or _sanitize_alias(dep.name)
49
+
50
+ # Track serve variables in order of appearance (deduped)
51
+ if dep.use_in_serve and alias not in serve_vars:
52
+ serve_vars.append(alias)
53
+ if dep.use_in_build and alias not in build_vars:
54
+ build_vars.append(alias)
55
+
56
+ # Only declare each dependency once
57
+ if alias in declared:
58
+ continue
59
+ declared.add(alias)
60
+
61
+ version_var = None
62
+ if dep.env_var:
63
+ default = f' or "{dep.default_version}"' if dep.default_version else ""
64
+ version_key = alias + "_version"
65
+ lines.append(f'{version_key} = getenv("{dep.env_var}"){default}')
66
+ version_var = version_key
67
+ if version_var:
68
+ lines.append(f'{alias} = dep("{dep.name}", {version_var})')
69
+ else:
70
+ lines.append(f'{alias} = dep("{dep.name}")')
71
+
72
+ return "\n".join(lines), serve_vars, build_vars
73
+
74
+
75
+ def _render_assets(assets: Optional[Dict[str, str]]) -> Optional[str]:
76
+ if not assets:
77
+ return None
78
+ inner = ",\n".join([f' "{k}": {v}' for k, v in assets.items()])
79
+ return f"{{\n{inner}\n }}"
80
+
81
+
82
+ def generate_shipit(path: Path) -> str:
83
+ provider = detect_provider(path)
84
+ provider.initialize(path)
85
+
86
+ # Collect parts
87
+ plan = ProviderPlan(
88
+ serve_name=provider.serve_name(path),
89
+ provider=provider.provider_kind(path),
90
+ declarations=provider.declarations(path),
91
+ dependencies=provider.dependencies(path),
92
+ build_steps=provider.build_steps(path),
93
+ prepare=provider.prepare_steps(path),
94
+ commands=provider.commands(path),
95
+ assets=provider.assets(path),
96
+ mounts=provider.mounts(path),
97
+ )
98
+
99
+ # Declare dependency variables (combined) and collect serve deps
100
+ dep_block, serve_dep_vars, build_dep_vars = _emit_dependencies_declarations(
101
+ plan.dependencies
102
+ )
103
+
104
+ # Compose serve(...) body
105
+ # Auto-insert a use(...) step at the beginning if not explicitly provided
106
+ build_steps: List[str] = list(plan.build_steps)
107
+ if build_dep_vars and not any("use(" in s for s in build_steps):
108
+ build_steps.insert(0, f"use({', '.join(build_dep_vars)})")
109
+
110
+ build_steps_block = ",\n".join([f" {s}" for s in build_steps])
111
+ deps_array = ", ".join(serve_dep_vars)
112
+ commands_lines = ",\n".join([f' "{k}": {v}' for k, v in plan.commands.items()])
113
+ assets_block = _render_assets(plan.assets)
114
+ mounts_block = None
115
+ if plan.mounts:
116
+ mounts_block = ",\n".join([f" {k}: {v}" for k, v in plan.mounts.items()])
117
+
118
+ out: List[str] = []
119
+ if dep_block:
120
+ out.append(dep_block)
121
+ out.append("")
122
+ if plan.declarations:
123
+ out.append(plan.declarations)
124
+ out.append("")
125
+ out.append("serve(")
126
+ out.append(f' name="{plan.serve_name}",')
127
+ out.append(f' provider="{plan.provider}",')
128
+ out.append(" build=[")
129
+ out.append(build_steps_block)
130
+ out.append(" ],")
131
+ if assets_block:
132
+ out.append(" assets=" + assets_block + ",")
133
+ out.append(f" deps=[{deps_array}],")
134
+ if plan.prepare:
135
+ prepare_steps_block = ",\n".join([f" {s}" for s in plan.prepare])
136
+ out.append(" prepare=[")
137
+ out.append(prepare_steps_block)
138
+ out.append(" ],")
139
+ out.append(" commands = {")
140
+ out.append(commands_lines)
141
+ out.append(" },")
142
+ if mounts_block:
143
+ out.append(" mounts={")
144
+ out.append(mounts_block)
145
+ out.append(" },")
146
+ out.append(")")
147
+ out.append("")
148
+ return "\n".join(out)
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional, Protocol
6
+
7
+
8
+ @dataclass
9
+ class DetectResult:
10
+ name: str
11
+ score: int # Higher score wins when multiple providers match
12
+
13
+
14
+ class Provider(Protocol):
15
+ def name(self) -> str: ...
16
+ def detect(self, path: Path) -> Optional[DetectResult]: ...
17
+ def initialize(self, path: Path) -> None: ...
18
+ # Structured plan steps
19
+ def serve_name(self, path: Path) -> str: ...
20
+ def provider_kind(self, path: Path) -> str: ...
21
+ def dependencies(self, path: Path) -> list["DependencySpec"]: ...
22
+ def declarations(self, path: Path) -> Optional[str]: ...
23
+ def build_steps(self, path: Path) -> list[str]: ...
24
+ # Prepare: list of Starlark step calls (currently only run(...))
25
+ def prepare_steps(self, path: Path) -> Optional[List[str]]: ...
26
+ def commands(self, path: Path) -> Dict[str, str]: ...
27
+ def assets(self, path: Path) -> Optional[Dict[str, str]]: ...
28
+ def mounts(self, path: Path) -> Optional[Dict[str, str]]: ...
29
+
30
+
31
+ @dataclass
32
+ class DependencySpec:
33
+ name: str
34
+ env_var: Optional[str] = None
35
+ default_version: Optional[str] = None
36
+ alias: Optional[str] = None # Variable name in Shipit plan
37
+ use_in_build: bool = False
38
+ use_in_serve: bool = False
39
+
40
+
41
+ @dataclass
42
+ class ProviderPlan:
43
+ serve_name: str
44
+ provider: str
45
+ declarations: Optional[str] = None
46
+ dependencies: List[DependencySpec] = field(default_factory=list)
47
+ build_steps: List[str] = field(default_factory=list)
48
+ prepare: Optional[List[str]] = None
49
+ commands: Dict[str, str] = field(default_factory=dict)
50
+ assets: Optional[Dict[str, str]] = None
51
+ mounts: Optional[Dict[str, str]] = None
52
+
53
+
54
+ def _exists(path: Path, *candidates: str) -> bool:
55
+ return any((path / c).exists() for c in candidates)
56
+
57
+
58
+ def _has_dependency(pkg_json: Path, dep: str) -> bool:
59
+ try:
60
+ import json
61
+
62
+ data = json.loads(pkg_json.read_text())
63
+ for section in ("dependencies", "devDependencies", "peerDependencies"):
64
+ if dep in data.get(section, {}):
65
+ return True
66
+ except Exception:
67
+ return False
68
+ return False
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependency
7
+
8
+
9
+ class GatsbyProvider:
10
+ def name(self) -> str:
11
+ return "gatsby"
12
+
13
+ def detect(self, path: Path) -> Optional[DetectResult]:
14
+ pkg = path / "package.json"
15
+ if not pkg.exists():
16
+ return None
17
+ if _exists(path, "gatsby-config.js", "gatsby-config.ts") or _has_dependency(
18
+ pkg, "gatsby"
19
+ ):
20
+ return DetectResult(self.name(), 90)
21
+ return None
22
+
23
+ def initialize(self, path: Path) -> None:
24
+ pass
25
+
26
+ def serve_name(self, path: Path) -> str:
27
+ return path.name
28
+
29
+ def provider_kind(self, path: Path) -> str:
30
+ return "staticsite"
31
+
32
+ def declarations(self, path: Path) -> Optional[str]:
33
+ return None
34
+
35
+ def dependencies(self, path: Path) -> list[DependencySpec]:
36
+ return [
37
+ DependencySpec(
38
+ "node",
39
+ env_var="SHIPIT_NODE_VERSION",
40
+ default_version="22",
41
+ use_in_build=True,
42
+ ),
43
+ DependencySpec("npm", use_in_build=True),
44
+ DependencySpec("static-web-server", env_var="SHIPIT_SWS_VERSION", use_in_serve=True),
45
+ ]
46
+
47
+ def build_steps(self, path: Path) -> list[str]:
48
+ return [
49
+ "run(\"npm install\", inputs=[\"package.json\", \"package-lock.json\"], group=\"install\")",
50
+ "copy(\".\", \".\", ignore=[\"node_modules\", \".git\"])",
51
+ "run(\"npm run build\", outputs=[\"public\"], group=\"build\")",
52
+ ]
53
+
54
+ def prepare_steps(self, path: Path) -> Optional[list[str]]:
55
+ return None
56
+
57
+ def commands(self, path: Path) -> Dict[str, str]:
58
+ return {"start": '"static-web-server --root /app/public"'}
59
+
60
+ def assets(self, path: Path) -> Optional[Dict[str, str]]:
61
+ return None
62
+
63
+ def mounts(self, path: Path) -> Optional[Dict[str, str]]:
64
+ return None
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ from .base import DetectResult, DependencySpec, Provider, _exists
7
+ from .staticfile import StaticFileProvider
8
+
9
+ class HugoProvider(StaticFileProvider):
10
+ static_dir = "public"
11
+
12
+ def name(self) -> str:
13
+ return "hugo"
14
+
15
+ def detect(self, path: Path) -> Optional[DetectResult]:
16
+ if _exists(path, "hugo.toml", "hugo.json", "hugo.yaml", "hugo.yml"):
17
+ return DetectResult(self.name(), 80)
18
+ if (
19
+ _exists(path, "config.toml", "config.json", "config.yaml", "config.yml")
20
+ and _exists(path, "content")
21
+ and (_exists(path, "static") or _exists(path, "themes"))
22
+ ):
23
+ return DetectResult(self.name(), 40)
24
+ return None
25
+
26
+ def serve_name(self, path: Path) -> str:
27
+ return path.name
28
+
29
+ def provider_kind(self, path: Path) -> str:
30
+ return "staticsite"
31
+
32
+ def dependencies(self, path: Path) -> list[DependencySpec]:
33
+ return [
34
+ DependencySpec(
35
+ "hugo",
36
+ env_var="SHIPIT_HUGO_VERSION",
37
+ default_version="0.149.0",
38
+ use_in_build=True,
39
+ ),
40
+ *super().dependencies(path),
41
+ ]
42
+
43
+ def build_steps(self, path: Path) -> list[str]:
44
+ return [
45
+ 'copy(".", ".", ignore=[".git"])',
46
+ 'run("hugo build", outputs=["public"], group="build")',
47
+ ]
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ from .base import DetectResult, DependencySpec, Provider, _exists
7
+
8
+
9
+ class LaravelProvider:
10
+ def name(self) -> str:
11
+ return "laravel"
12
+
13
+ def detect(self, path: Path) -> Optional[DetectResult]:
14
+ if _exists(path, "artisan") and _exists(path, "composer.json"):
15
+ return DetectResult(self.name(), 95)
16
+ return None
17
+
18
+ def initialize(self, path: Path) -> None:
19
+ pass
20
+
21
+ def serve_name(self, path: Path) -> str:
22
+ return path.name
23
+
24
+ def provider_kind(self, path: Path) -> str:
25
+ return "php"
26
+
27
+ def dependencies(self, path: Path) -> list[DependencySpec]:
28
+ return [
29
+ DependencySpec(
30
+ "php",
31
+ env_var="SHIPIT_PHP_VERSION",
32
+ default_version="8.3",
33
+ use_in_build=True,
34
+ use_in_serve=True,
35
+ ),
36
+ DependencySpec("composer", use_in_build=True),
37
+ DependencySpec("pie", use_in_build=True),
38
+ DependencySpec("pnpm", use_in_build=True),
39
+ DependencySpec("bash", use_in_serve=True),
40
+ ]
41
+
42
+ def declarations(self, path: Path) -> Optional[str]:
43
+ return "HOME = getenv(\"HOME\")"
44
+
45
+ def build_steps(self, path: Path) -> list[str]:
46
+ return [
47
+ "env(HOME=HOME, COMPOSER_FUND=\"0\")",
48
+ "run(\"pie install php/pdo_pgsql\")",
49
+ "run(\"composer install --optimize-autoloader --no-scripts --no-interaction\", inputs=[\"composer.json\", \"composer.lock\", \"artisan\"], outputs=[\".\"], group=\"install\")",
50
+ "run(\"pnpm install\", inputs=[\"package.json\", \"package-lock.json\"], outputs=[\".\"], group=\"install\")",
51
+ "copy(\".\", \".\", ignore=[\".git\"])",
52
+ "run(\"pnpm run build\", outputs=[\".\"], group=\"build\")",
53
+ ]
54
+
55
+ def prepare_steps(self, path: Path) -> Optional[list[str]]:
56
+ return [
57
+ 'run("mkdir -p storage/framework/{sessions,views,cache,testing} storage/logs bootstrap/cache")',
58
+ 'run("php artisan config:cache")',
59
+ 'run("php artisan event:cache")',
60
+ 'run("php artisan route:cache")',
61
+ 'run("php artisan view:cache")',
62
+ ]
63
+
64
+ def commands(self, path: Path) -> Dict[str, str]:
65
+ return {
66
+ "start": '"php -S localhost:8080 -t public"',
67
+ "after_deploy": '"php artisan migrate"',
68
+ }
69
+
70
+ def assets(self, path: Path) -> Optional[Dict[str, str]]:
71
+ return None
72
+
73
+ def mounts(self, path: Path) -> Optional[Dict[str, str]]:
74
+ return None
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ from .base import DetectResult, DependencySpec, Provider, _exists
7
+
8
+
9
+ class MkdocsProvider:
10
+ def name(self) -> str:
11
+ return "mkdocs"
12
+
13
+ def detect(self, path: Path) -> Optional[DetectResult]:
14
+ if _exists(path, "mkdocs.yml", "mkdocs.yaml"):
15
+ return DetectResult(self.name(), 85)
16
+ return None
17
+
18
+ def initialize(self, path: Path) -> None:
19
+ pass
20
+
21
+ def serve_name(self, path: Path) -> str:
22
+ return path.name
23
+
24
+ def provider_kind(self, path: Path) -> str:
25
+ return "mkdocs-site"
26
+
27
+ def dependencies(self, path: Path) -> list[DependencySpec]:
28
+ return [
29
+ DependencySpec(
30
+ "python",
31
+ env_var="SHIPIT_PYTHON_VERSION",
32
+ default_version="3.13",
33
+ use_in_build=True,
34
+ ),
35
+ DependencySpec(
36
+ "uv",
37
+ env_var="SHIPIT_UV_VERSION",
38
+ default_version="0.8.15",
39
+ use_in_build=True,
40
+ ),
41
+ DependencySpec(
42
+ "static-web-server",
43
+ env_var="SHIPIT_SWS_VERSION",
44
+ default_version="2.38.0",
45
+ use_in_serve=True,
46
+ ),
47
+ ]
48
+
49
+ def declarations(self, path: Path) -> Optional[str]:
50
+ return None
51
+
52
+ def build_steps(self, path: Path) -> list[str]:
53
+ has_requirements = _exists(path, "requirements.txt")
54
+ if has_requirements:
55
+ install_lines = [
56
+ "run(\"uv init --no-managed-python\", inputs=[], outputs=[\".\"], group=\"install\")",
57
+ "run(f\"uv add -r requirements.txt\", inputs=[\"requirements.txt\"], outputs=[\".venv\"], group=\"install\")",
58
+ ]
59
+ else:
60
+ install_lines = [
61
+ "mkdocs_version = getenv(\"SHIPIT_MKDOCS_VERSION\") or \"1.6.1\"",
62
+ "run(\"uv init --no-managed-python\", inputs=[], outputs=[\".venv\"], group=\"install\")",
63
+ "run(f\"uv add mkdocs=={mkdocs_version}\", group=\"install\")",
64
+ ]
65
+ return [
66
+ *install_lines,
67
+ "copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
68
+ "run(\"uv run mkdocs build\", outputs=[\".\"], group=\"build\")",
69
+ ]
70
+
71
+ def prepare_steps(self, path: Path) -> Optional[list[str]]:
72
+ return None
73
+
74
+ def commands(self, path: Path) -> Dict[str, str]:
75
+ return {"start": '"static-web-server --root {}".format(buildpath("site"))'}
76
+
77
+ def assets(self, path: Path) -> Optional[Dict[str, str]]:
78
+ return None
79
+
80
+ def mounts(self, path: Path) -> Optional[Dict[str, str]]:
81
+ return None
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependency
7
+
8
+
9
+ class NodeStaticProvider:
10
+ def name(self) -> str:
11
+ return "node-static"
12
+
13
+ def detect(self, path: Path) -> Optional[DetectResult]:
14
+ pkg = path / "package.json"
15
+ if not pkg.exists():
16
+ return None
17
+ static_generators = ["astro", "vite", "next", "nuxt"]
18
+ if any(_has_dependency(pkg, dep) for dep in static_generators):
19
+ return DetectResult(self.name(), 40)
20
+ return None
21
+
22
+ def initialize(self, path: Path) -> None:
23
+ pass
24
+
25
+ def serve_name(self, path: Path) -> str:
26
+ return path.name
27
+
28
+ def provider_kind(self, path: Path) -> str:
29
+ return "staticsite"
30
+
31
+ def dependencies(self, path: Path) -> list[DependencySpec]:
32
+ return [
33
+ DependencySpec(
34
+ "node",
35
+ env_var="SHIPIT_NODE_VERSION",
36
+ default_version="22",
37
+ use_in_build=True,
38
+ ),
39
+ DependencySpec("npm", use_in_build=True),
40
+ DependencySpec("static-web-server", use_in_serve=True),
41
+ ]
42
+
43
+ def declarations(self, path: Path) -> Optional[str]:
44
+ return None
45
+
46
+ def build_steps(self, path: Path) -> list[str]:
47
+ output_dir = "dist" if (path / "dist").exists() else "public"
48
+ return [
49
+ "run(\"npm install\", inputs=[\"package.json\", \"package-lock.json\"], group=\"install\")",
50
+ "copy(\".\", \".\", ignore=[\"node_modules\", \".git\"])",
51
+ f"run(\"npm run build\", outputs=[\"{output_dir}\"], group=\"build\")",
52
+ ]
53
+
54
+ def prepare_steps(self, path: Path) -> Optional[list[str]]:
55
+ return None
56
+
57
+ def commands(self, path: Path) -> Dict[str, str]:
58
+ output_dir = "dist" if (path / "dist").exists() else "public"
59
+ return {"start": f'"static-web-server --root /app/{output_dir}"'}
60
+
61
+ def assets(self, path: Path) -> Optional[Dict[str, str]]:
62
+ return None
63
+
64
+ def mounts(self, path: Path) -> Optional[Dict[str, str]]:
65
+ return None
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional
5
+
6
+ from .base import DetectResult, DependencySpec, Provider, _exists
7
+
8
+
9
+ class PhpProvider:
10
+ def name(self) -> str:
11
+ return "php"
12
+
13
+ def detect(self, path: Path) -> Optional[DetectResult]:
14
+ if _exists(path, "composer.json") and _exists(path, "public/index.php"):
15
+ return DetectResult(self.name(), 60)
16
+ if _exists(path, "index.php") and not _exists(path, "composer.json"):
17
+ return DetectResult(self.name(), 10)
18
+ return None
19
+
20
+ def initialize(self, path: Path) -> None:
21
+ pass
22
+
23
+ def serve_name(self, path: Path) -> str:
24
+ return path.name
25
+
26
+ def provider_kind(self, path: Path) -> str:
27
+ return "php"
28
+
29
+ def has_composer(self, path: Path) -> bool:
30
+ return _exists(path, "composer.json", "composer.lock")
31
+
32
+ def dependencies(self, path: Path) -> list[DependencySpec]:
33
+ deps = [
34
+ DependencySpec(
35
+ "php",
36
+ env_var="SHIPIT_PHP_VERSION",
37
+ default_version="8.3",
38
+ use_in_build=True,
39
+ use_in_serve=True,
40
+ ),
41
+ ]
42
+ if self.has_composer(path):
43
+ deps.append(DependencySpec("composer", use_in_build=True))
44
+ deps.append(DependencySpec("bash", use_in_serve=True))
45
+ return deps
46
+
47
+ def declarations(self, path: Path) -> Optional[str]:
48
+ return "HOME = getenv(\"HOME\")"
49
+
50
+ def build_steps(self, path: Path) -> list[str]:
51
+ steps = []
52
+
53
+ if self.has_composer(path):
54
+ steps.append("env(HOME=HOME, COMPOSER_FUND=\"0\")")
55
+ steps.append("run(\"composer install --optimize-autoloader --no-scripts --no-interaction\", inputs=[\"composer.json\", \"composer.lock\"], outputs=[\".\"], group=\"install\")")
56
+
57
+ steps.append("copy(\".\", \".\", ignore=[\".git\"])")
58
+ return steps
59
+
60
+ def prepare_steps(self, path: Path) -> Optional[list[str]]:
61
+ return None
62
+
63
+ def commands(self, path: Path) -> Dict[str, str]:
64
+ if _exists(path, "public/index.php"):
65
+ return {"start": '"php -S localhost:8080 -t public"'}
66
+ elif _exists(path, "index.php"):
67
+ return {"start": '"php -S localhost:8080" -t .'}
68
+
69
+ def assets(self, path: Path) -> Optional[Dict[str, str]]:
70
+ return {"php.ini": "get_asset(\"php/php.ini\")"}
71
+
72
+ def mounts(self, path: Path) -> Optional[Dict[str, str]]:
73
+ return None