quoll-cli 0.2.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.
- quoll/__init__.py +0 -0
- quoll/cd/__init__.py +1 -0
- quoll/cd/config.py +75 -0
- quoll/ci/__init__.py +0 -0
- quoll/ci/config.py +213 -0
- quoll/ci/custom.py +32 -0
- quoll/ci/detect.py +139 -0
- quoll/ci/steps.py +85 -0
- quoll/ci/tools.py +104 -0
- quoll/commands/__init__.py +0 -0
- quoll/commands/_common.py +145 -0
- quoll/commands/_workspace.py +83 -0
- quoll/commands/add.py +104 -0
- quoll/commands/add_secret.py +75 -0
- quoll/commands/cd.py +214 -0
- quoll/commands/ci.py +521 -0
- quoll/commands/docker.py +244 -0
- quoll/commands/k8s.py +211 -0
- quoll/commands/new.py +337 -0
- quoll/commands/remove.py +79 -0
- quoll/config/__init__.py +0 -0
- quoll/config/quoll_toml.py +196 -0
- quoll/config/settings.py +22 -0
- quoll/config/store.py +39 -0
- quoll/console.py +3 -0
- quoll/docker/__init__.py +1 -0
- quoll/docker/config.py +64 -0
- quoll/docker/detect.py +41 -0
- quoll/docker/render.py +162 -0
- quoll/files.py +64 -0
- quoll/k8s/__init__.py +0 -0
- quoll/k8s/chart/templates/_helpers.tpl +38 -0
- quoll/k8s/chart/templates/deployment.yaml +93 -0
- quoll/k8s/chart/templates/hpa.yaml +24 -0
- quoll/k8s/chart/templates/ingress.yaml +35 -0
- quoll/k8s/chart/templates/pdb.yaml +16 -0
- quoll/k8s/chart/templates/service.yaml +18 -0
- quoll/k8s/chart/templates/serviceaccount.yaml +10 -0
- quoll/k8s/ci.py +39 -0
- quoll/k8s/config.py +97 -0
- quoll/k8s/deploy.py +22 -0
- quoll/k8s/detect.py +32 -0
- quoll/k8s/render.py +94 -0
- quoll/languages/__init__.py +121 -0
- quoll/languages/base.py +350 -0
- quoll/languages/cpp/Dockerfile.j2 +37 -0
- quoll/languages/cpp/__init__.py +17 -0
- quoll/languages/cpp/ci.py +192 -0
- quoll/languages/cpp/detect.py +84 -0
- quoll/languages/cpp/docker.py +47 -0
- quoll/languages/dotnet/Dockerfile.j2 +21 -0
- quoll/languages/dotnet/__init__.py +17 -0
- quoll/languages/dotnet/ci.py +88 -0
- quoll/languages/dotnet/detect.py +53 -0
- quoll/languages/dotnet/docker.py +47 -0
- quoll/languages/go/Dockerfile.j2 +25 -0
- quoll/languages/go/__init__.py +17 -0
- quoll/languages/go/ci.py +126 -0
- quoll/languages/go/detect.py +30 -0
- quoll/languages/go/docker.py +40 -0
- quoll/languages/java/Dockerfile.j2 +28 -0
- quoll/languages/java/__init__.py +20 -0
- quoll/languages/java/ci.py +163 -0
- quoll/languages/java/detect.py +77 -0
- quoll/languages/java/docker.py +45 -0
- quoll/languages/java/frameworks/__init__.py +1 -0
- quoll/languages/java/frameworks/spring_boot/Dockerfile.j2 +18 -0
- quoll/languages/java/frameworks/spring_boot/__init__.py +10 -0
- quoll/languages/java/frameworks/spring_boot/detect.py +9 -0
- quoll/languages/java/frameworks/spring_boot/docker.py +19 -0
- quoll/languages/java/frameworks/spring_boot/services.py +24 -0
- quoll/languages/node/Dockerfile.j2 +98 -0
- quoll/languages/node/__init__.py +29 -0
- quoll/languages/node/ci.py +200 -0
- quoll/languages/node/detect.py +164 -0
- quoll/languages/node/docker.py +141 -0
- quoll/languages/node/frameworks/__init__.py +1 -0
- quoll/languages/node/frameworks/angular/__init__.py +18 -0
- quoll/languages/node/frameworks/angular/ci.py +34 -0
- quoll/languages/node/frameworks/angular/detect.py +20 -0
- quoll/languages/node/frameworks/angular/docker.py +31 -0
- quoll/languages/node/frameworks/angular/starter.py +74 -0
- quoll/languages/node/frameworks/astro/__init__.py +10 -0
- quoll/languages/node/frameworks/astro/ci.py +17 -0
- quoll/languages/node/frameworks/astro/detect.py +14 -0
- quoll/languages/node/frameworks/astro/docker.py +17 -0
- quoll/languages/node/frameworks/nestjs/__init__.py +7 -0
- quoll/languages/node/frameworks/nestjs/detect.py +9 -0
- quoll/languages/node/frameworks/nestjs/docker.py +10 -0
- quoll/languages/node/frameworks/nextjs/Dockerfile.j2 +14 -0
- quoll/languages/node/frameworks/nextjs/__init__.py +8 -0
- quoll/languages/node/frameworks/nextjs/ci.py +29 -0
- quoll/languages/node/frameworks/nextjs/detect.py +13 -0
- quoll/languages/node/frameworks/nextjs/docker.py +41 -0
- quoll/languages/node/frameworks/nuxt/__init__.py +10 -0
- quoll/languages/node/frameworks/nuxt/ci.py +17 -0
- quoll/languages/node/frameworks/nuxt/detect.py +17 -0
- quoll/languages/node/frameworks/nuxt/docker.py +24 -0
- quoll/languages/node/frameworks/sveltekit/__init__.py +15 -0
- quoll/languages/node/frameworks/sveltekit/ci.py +15 -0
- quoll/languages/node/frameworks/sveltekit/detect.py +18 -0
- quoll/languages/node/frameworks/sveltekit/docker.py +18 -0
- quoll/languages/node/starter.py +42 -0
- quoll/languages/node/templates/express/README.md +11 -0
- quoll/languages/node/templates/express/env.example +2 -0
- quoll/languages/node/templates/express/eslint.config.js +15 -0
- quoll/languages/node/templates/express/gitignore +4 -0
- quoll/languages/node/templates/express/nvmrc +1 -0
- quoll/languages/node/templates/express/package.json +16 -0
- quoll/languages/node/templates/express/prettierignore +6 -0
- quoll/languages/node/templates/express/src/app.ts +27 -0
- quoll/languages/node/templates/express/src/server.ts +11 -0
- quoll/languages/node/templates/express/test/app.test.ts +29 -0
- quoll/languages/node/templates/express/tsconfig.build.json +8 -0
- quoll/languages/node/templates/express/tsconfig.json +14 -0
- quoll/languages/node/templates/express/vitest.config.ts +12 -0
- quoll/languages/php/Dockerfile.j2 +40 -0
- quoll/languages/php/__init__.py +19 -0
- quoll/languages/php/ci.py +147 -0
- quoll/languages/php/detect.py +75 -0
- quoll/languages/php/docker.py +59 -0
- quoll/languages/php/frameworks/__init__.py +1 -0
- quoll/languages/php/frameworks/laravel/__init__.py +17 -0
- quoll/languages/php/frameworks/laravel/ci.py +24 -0
- quoll/languages/php/frameworks/laravel/detect.py +13 -0
- quoll/languages/php/frameworks/laravel/docker.py +29 -0
- quoll/languages/php/frameworks/laravel/services.py +25 -0
- quoll/languages/php/frameworks/symfony/__init__.py +15 -0
- quoll/languages/php/frameworks/symfony/ci.py +22 -0
- quoll/languages/php/frameworks/symfony/detect.py +17 -0
- quoll/languages/php/frameworks/symfony/docker.py +25 -0
- quoll/languages/python/Dockerfile.j2 +61 -0
- quoll/languages/python/__init__.py +19 -0
- quoll/languages/python/ci.py +157 -0
- quoll/languages/python/detect.py +175 -0
- quoll/languages/python/docker.py +80 -0
- quoll/languages/python/frameworks/__init__.py +1 -0
- quoll/languages/python/frameworks/django/__init__.py +16 -0
- quoll/languages/python/frameworks/django/ci.py +43 -0
- quoll/languages/python/frameworks/django/detect.py +17 -0
- quoll/languages/python/frameworks/django/docker.py +20 -0
- quoll/languages/python/frameworks/fastapi/__init__.py +7 -0
- quoll/languages/python/frameworks/fastapi/detect.py +9 -0
- quoll/languages/python/frameworks/fastapi/docker.py +21 -0
- quoll/languages/python/frameworks/flask/__init__.py +7 -0
- quoll/languages/python/frameworks/flask/detect.py +9 -0
- quoll/languages/python/frameworks/flask/docker.py +22 -0
- quoll/languages/ruby/Dockerfile.j2 +45 -0
- quoll/languages/ruby/__init__.py +19 -0
- quoll/languages/ruby/ci.py +110 -0
- quoll/languages/ruby/detect.py +72 -0
- quoll/languages/ruby/docker.py +63 -0
- quoll/languages/ruby/frameworks/__init__.py +1 -0
- quoll/languages/ruby/frameworks/rails/Dockerfile.j2 +16 -0
- quoll/languages/ruby/frameworks/rails/__init__.py +18 -0
- quoll/languages/ruby/frameworks/rails/ci.py +26 -0
- quoll/languages/ruby/frameworks/rails/detect.py +15 -0
- quoll/languages/ruby/frameworks/rails/docker.py +62 -0
- quoll/languages/ruby/frameworks/rails/services.py +10 -0
- quoll/languages/rust/Dockerfile.j2 +23 -0
- quoll/languages/rust/__init__.py +17 -0
- quoll/languages/rust/ci.py +125 -0
- quoll/languages/rust/detect.py +46 -0
- quoll/languages/rust/docker.py +38 -0
- quoll/main.py +50 -0
- quoll/platforms/__init__.py +62 -0
- quoll/platforms/base.py +103 -0
- quoll/platforms/github/__init__.py +4 -0
- quoll/platforms/github/actions.py +78 -0
- quoll/platforms/github/api.py +124 -0
- quoll/platforms/github/cd.py +265 -0
- quoll/platforms/github/ci.py +497 -0
- quoll/platforms/github/dependabot.yml.j2 +49 -0
- quoll/platforms/github/platform.py +52 -0
- quoll/platforms/github/workflow.py +74 -0
- quoll/platforms/gitlab/__init__.py +4 -0
- quoll/platforms/gitlab/api.py +158 -0
- quoll/platforms/gitlab/cd.py +226 -0
- quoll/platforms/gitlab/ci.py +400 -0
- quoll/platforms/gitlab/pipeline.py +136 -0
- quoll/platforms/gitlab/platform.py +54 -0
- quoll/project.py +60 -0
- quoll/services.py +117 -0
- quoll/starters.py +117 -0
- quoll/workspace.py +232 -0
- quoll/yaml_dump.py +137 -0
- quoll_cli-0.2.0.dist-info/METADATA +641 -0
- quoll_cli-0.2.0.dist-info/RECORD +191 -0
- quoll_cli-0.2.0.dist-info/WHEEL +4 -0
- quoll_cli-0.2.0.dist-info/entry_points.txt +2 -0
- quoll_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{% block base %}
|
|
2
|
+
FROM dunglas/frankenphp:1-php{{ cfg.version }} AS base
|
|
3
|
+
{{ os_updates }}
|
|
4
|
+
{% if extensions %}
|
|
5
|
+
RUN install-php-extensions {{ extensions | join(" ") }}
|
|
6
|
+
{% endif %}
|
|
7
|
+
{% if env %}
|
|
8
|
+
ENV {% for key, value in env.items() %}{{ key }}={{ value }}{{ " \\\n " if not loop.last }}{% endfor %}
|
|
9
|
+
|
|
10
|
+
{% endif %}
|
|
11
|
+
WORKDIR /app
|
|
12
|
+
{% endblock %}
|
|
13
|
+
|
|
14
|
+
{% block build %}
|
|
15
|
+
FROM base AS build
|
|
16
|
+
RUN install-php-extensions @composer
|
|
17
|
+
ENV COMPOSER_CACHE_DIR=/tmp/composer-cache
|
|
18
|
+
# Dependencies first: code changes don't reinstall them.
|
|
19
|
+
COPY composer.json {{ "composer.lock " if lockfile }}./
|
|
20
|
+
RUN --mount=type=cache,target=/tmp/composer-cache \
|
|
21
|
+
composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts --no-autoloader
|
|
22
|
+
COPY . .
|
|
23
|
+
RUN composer dump-autoload --no-dev --optimize
|
|
24
|
+
{% for command in post_build %}
|
|
25
|
+
RUN {{ command }}
|
|
26
|
+
{% endfor %}
|
|
27
|
+
{% endblock %}
|
|
28
|
+
|
|
29
|
+
{% block runtime %}
|
|
30
|
+
FROM base
|
|
31
|
+
# Plain HTTP on an unprivileged port: the container runs as non-root, a proxy terminates TLS.
|
|
32
|
+
ENV SERVER_NAME=:{{ cfg.port }}{{ " \\\n SERVER_ROOT=/app" if not public }}
|
|
33
|
+
# A numeric user: Kubernetes can then check it isn't root (runAsNonRoot).
|
|
34
|
+
RUN useradd --uid 10001 --no-create-home app \
|
|
35
|
+
&& setcap -r /usr/local/bin/frankenphp \
|
|
36
|
+
&& chown -R app:app /config/caddy /data/caddy
|
|
37
|
+
COPY --from=build --chown=app:app /app /app
|
|
38
|
+
USER 10001
|
|
39
|
+
EXPOSE {{ cfg.port }}
|
|
40
|
+
{% endblock %}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""PHP: Composer projects."""
|
|
2
|
+
|
|
3
|
+
from quoll.languages.base import Language
|
|
4
|
+
from quoll.languages.php.ci import PhpCI
|
|
5
|
+
from quoll.languages.php.detect import detect
|
|
6
|
+
from quoll.languages.php.docker import PhpImage
|
|
7
|
+
from quoll.languages.php.frameworks import laravel, symfony
|
|
8
|
+
|
|
9
|
+
language = Language(
|
|
10
|
+
name="php",
|
|
11
|
+
markers=("composer.json",),
|
|
12
|
+
package_managers=("composer",),
|
|
13
|
+
lint_tools=("php-cs-fixer", "phpcs", "pint", "phpstan", "psalm", "rector"),
|
|
14
|
+
test_runners=("phpunit", "pest"),
|
|
15
|
+
detect=detect,
|
|
16
|
+
ci=PhpCI(),
|
|
17
|
+
frameworks=(symfony.framework, laravel.framework),
|
|
18
|
+
docker=PhpImage(),
|
|
19
|
+
)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence
|
|
4
|
+
|
|
5
|
+
from quoll.ci.steps import ROOT, Cache, Item, Step, on_github, step
|
|
6
|
+
from quoll.languages.base import THRESHOLD_REGEX, CISteps, TestReports
|
|
7
|
+
from quoll.platforms.github.actions import ACTIONS
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from quoll.ci.config import CIConfig
|
|
11
|
+
|
|
12
|
+
# tool: (step name, command, flag printing GitHub annotations)
|
|
13
|
+
_LINT = {
|
|
14
|
+
"php-cs-fixer": ("PHP CS Fixer", "vendor/bin/php-cs-fixer fix --dry-run --diff", ""),
|
|
15
|
+
"phpcs": ("PHP_CodeSniffer", "vendor/bin/phpcs", ""),
|
|
16
|
+
"pint": ("Pint", "vendor/bin/pint --test", ""),
|
|
17
|
+
"phpstan": ("PHPStan", "vendor/bin/phpstan analyse --no-progress", " --error-format=github"),
|
|
18
|
+
"psalm": ("Psalm", "vendor/bin/psalm --no-progress", " --output-format=github"),
|
|
19
|
+
"rector": ("Rector", "vendor/bin/rector process --dry-run --no-progress-bar", ""),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# PHPUnit has no CLI threshold: read the line rate from its Clover report.
|
|
23
|
+
_CLOVER_CHECK = """# shellcheck disable=SC2016 # PHP code: its $ are PHP's
|
|
24
|
+
php -r '
|
|
25
|
+
$m = simplexml_load_file("coverage.xml")->project->metrics;
|
|
26
|
+
$pct = (int) $m["statements"] ? 100 * (int) $m["coveredstatements"] / (int) $m["statements"] : 100;
|
|
27
|
+
$summary = getenv("GITHUB_STEP_SUMMARY");
|
|
28
|
+
if ($summary) file_put_contents($summary, sprintf("### Coverage: %.1f%%\\n", $pct), FILE_APPEND);
|
|
29
|
+
if ($pct < {threshold}) {{ printf("::error::Coverage %.1f%% is below the {threshold}%% threshold\\n", $pct); exit(1); }}
|
|
30
|
+
printf("Coverage %.1f%% (threshold {threshold}%%)\\n", $pct);'"""
|
|
31
|
+
|
|
32
|
+
# The official images have neither Composer nor most extensions: install them, then the ext-* the lock file needs.
|
|
33
|
+
_EXTENSIONS = """curl -sSLf -o /usr/local/bin/install-php-extensions \\
|
|
34
|
+
https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions
|
|
35
|
+
chmod +x /usr/local/bin/install-php-extensions
|
|
36
|
+
install-php-extensions @composer zip{coverage}
|
|
37
|
+
if [ -f composer.lock ]; then
|
|
38
|
+
# Exits 2 when something is missing, which is the point here (the runner sets pipefail).
|
|
39
|
+
missing=$({{ composer check-platform-reqs --lock --no-interaction 2>/dev/null || true; }} \\
|
|
40
|
+
| awk '$1 ~ /^ext-/ && /missing/ {{ sub("ext-", "", $1); print $1 }}')
|
|
41
|
+
if [ -n "$missing" ]; then install-php-extensions $missing; fi
|
|
42
|
+
fi"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PhpCI(CISteps):
|
|
46
|
+
version_key = "php-version"
|
|
47
|
+
|
|
48
|
+
def setup(self, cfg: CIConfig, version: Optional[str]) -> List[Item]:
|
|
49
|
+
with_: Dict[str, Any] = (
|
|
50
|
+
{"php-version": version} if version else {"php-version-file": cfg.path(cfg.version_file)}
|
|
51
|
+
)
|
|
52
|
+
with_["coverage"] = "pcov" if cfg.test.enabled and cfg.test.coverage else "none"
|
|
53
|
+
with_["tools"] = "composer:v2"
|
|
54
|
+
steps = [step("Set up PHP", uses=ACTIONS["setup-php"], with_=with_)]
|
|
55
|
+
if cfg.cache:
|
|
56
|
+
steps += [
|
|
57
|
+
step(
|
|
58
|
+
"Get Composer cache directory",
|
|
59
|
+
id_="composer-cache",
|
|
60
|
+
run='echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"',
|
|
61
|
+
),
|
|
62
|
+
step(
|
|
63
|
+
"Cache Composer packages",
|
|
64
|
+
uses=ACTIONS["cache"],
|
|
65
|
+
with_={
|
|
66
|
+
"path": "${{ steps.composer-cache.outputs.dir }}",
|
|
67
|
+
"key": "${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}",
|
|
68
|
+
"restore-keys": "${{ runner.os }}-composer-",
|
|
69
|
+
},
|
|
70
|
+
),
|
|
71
|
+
]
|
|
72
|
+
return steps
|
|
73
|
+
|
|
74
|
+
def image(self, cfg: CIConfig, version: str) -> str:
|
|
75
|
+
return f"php:{version}-cli"
|
|
76
|
+
|
|
77
|
+
def image_setup(self, cfg: CIConfig) -> List[Step]:
|
|
78
|
+
coverage = " pcov" if cfg.test.enabled and cfg.test.coverage else ""
|
|
79
|
+
return [step("Install Composer and PHP extensions", run=_EXTENSIONS.format(coverage=coverage))]
|
|
80
|
+
|
|
81
|
+
def cache(self, cfg: CIConfig) -> Optional[Cache]:
|
|
82
|
+
if not cfg.cache:
|
|
83
|
+
return None
|
|
84
|
+
env = {"COMPOSER_CACHE_DIR": f"{ROOT}/.cache/composer"}
|
|
85
|
+
return Cache("composer", (".cache/composer",), key_files=("composer.lock",), env=env)
|
|
86
|
+
|
|
87
|
+
def install(self, cfg: CIConfig, packages: Sequence[str] = ()) -> List[Item]:
|
|
88
|
+
return [step("Install dependencies", run=cfg.install)]
|
|
89
|
+
|
|
90
|
+
def lint(self, cfg: CIConfig, tools: List[str]) -> List[Item]:
|
|
91
|
+
steps = self.install(cfg)
|
|
92
|
+
for tool in tools:
|
|
93
|
+
name, cmd, github = _LINT[tool]
|
|
94
|
+
steps.append(step(name, run=cmd + on_github(cfg, github)))
|
|
95
|
+
return steps
|
|
96
|
+
|
|
97
|
+
def test(self, cfg: CIConfig) -> List[Item]:
|
|
98
|
+
t = cfg.test
|
|
99
|
+
if t.runner == "pest":
|
|
100
|
+
cmd = "vendor/bin/pest"
|
|
101
|
+
if t.coverage:
|
|
102
|
+
cmd += f" --coverage --min={t.coverage_threshold} --coverage-clover coverage.xml"
|
|
103
|
+
cmd += report_flags(cfg)
|
|
104
|
+
name = "Tests" + (" with coverage" if t.coverage else "")
|
|
105
|
+
return [*self.install(cfg), step(name, run=cmd, secrets=t.env_secrets)]
|
|
106
|
+
if not t.coverage:
|
|
107
|
+
cmd = "vendor/bin/phpunit" + report_flags(cfg)
|
|
108
|
+
return [*self.install(cfg), step("Tests", run=cmd, secrets=t.env_secrets)]
|
|
109
|
+
cmd = "vendor/bin/phpunit --coverage-clover coverage.xml" + report_flags(cfg)
|
|
110
|
+
return [
|
|
111
|
+
*self.install(cfg),
|
|
112
|
+
step("Tests with coverage", run=cmd, secrets=t.env_secrets),
|
|
113
|
+
step("Coverage threshold", run=clover_check(t.coverage_threshold)),
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
def coverage_file(self, cfg: CIConfig) -> str:
|
|
117
|
+
return "coverage.xml"
|
|
118
|
+
|
|
119
|
+
def reports(self, cfg: CIConfig) -> TestReports:
|
|
120
|
+
coverage = cfg.test.coverage
|
|
121
|
+
# PHPUnit prints the threshold line of clover_check; Pest and `artisan test` their own total.
|
|
122
|
+
regex = THRESHOLD_REGEX if cfg.test.runner == "phpunit" else r"/Total:\s+(\d+(?:\.\d+)?) ?%/"
|
|
123
|
+
return TestReports(
|
|
124
|
+
junit="junit.xml",
|
|
125
|
+
cobertura="cobertura.xml" if coverage else "",
|
|
126
|
+
coverage_regex=regex if coverage else "",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def audit(self, cfg: CIConfig) -> List[Item]:
|
|
130
|
+
return [step("Dependency vulnerability scan", run="composer audit --locked --no-dev")]
|
|
131
|
+
|
|
132
|
+
def build(self, cfg: CIConfig) -> List[Item]:
|
|
133
|
+
return [*self.install(cfg), step("Build", run=cfg.build.command)]
|
|
134
|
+
|
|
135
|
+
def dependabot_ecosystem(self, cfg: CIConfig) -> str:
|
|
136
|
+
return "composer"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def report_flags(cfg: CIConfig) -> str:
|
|
140
|
+
"""PHPUnit (and Pest, Laravel's `artisan test`) options writing the JUnit and Cobertura reports."""
|
|
141
|
+
if not cfg.test.reports:
|
|
142
|
+
return ""
|
|
143
|
+
return " --log-junit junit.xml" + (" --coverage-cobertura cobertura.xml" if cfg.test.coverage else "")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def clover_check(threshold: int) -> str:
|
|
147
|
+
return _CLOVER_CHECK.format(threshold=threshold)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
from quoll.languages.base import Detection
|
|
7
|
+
from quoll.project import Project
|
|
8
|
+
from quoll.services import detect_services
|
|
9
|
+
|
|
10
|
+
PHP_SUPPORTED = ("8.2", "8.3", "8.4", "8.5")
|
|
11
|
+
|
|
12
|
+
# composer package: lint tool
|
|
13
|
+
_LINT_PACKAGES = {
|
|
14
|
+
"friendsofphp/php-cs-fixer": "php-cs-fixer",
|
|
15
|
+
"squizlabs/php_codesniffer": "phpcs",
|
|
16
|
+
"laravel/pint": "pint",
|
|
17
|
+
"phpstan/phpstan": "phpstan",
|
|
18
|
+
"vimeo/psalm": "psalm",
|
|
19
|
+
"rector/rector": "rector",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Packages and extensions revealing a backing service.
|
|
24
|
+
_SERVICE_PACKAGES = {
|
|
25
|
+
"postgres": ("ext-pdo_pgsql", "ext-pgsql"),
|
|
26
|
+
"mysql": ("ext-pdo_mysql", "ext-mysqli"),
|
|
27
|
+
"redis": ("predis/predis", "ext-redis", "snc/redis-bundle"),
|
|
28
|
+
"mongodb": ("mongodb/mongodb", "ext-mongodb", "doctrine/mongodb-odm"),
|
|
29
|
+
}
|
|
30
|
+
# Database settings in Symfony's .env / Laravel's .env.example.
|
|
31
|
+
_ENV_DATABASES = {
|
|
32
|
+
"ext-pdo_pgsql": r"^(DATABASE_URL=\W*postgres|DB_CONNECTION=pgsql)",
|
|
33
|
+
"ext-pdo_mysql": r"^(DATABASE_URL=\W*(mysql|mariadb)|DB_CONNECTION=(mysql|mariadb))",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def detect(project: Project) -> Detection:
|
|
38
|
+
det = Detection(package_manager="composer", install="composer install --no-interaction --no-progress --prefer-dist")
|
|
39
|
+
det.lockfile = "composer.lock" if project.exists("composer.lock") else ""
|
|
40
|
+
if not det.lockfile:
|
|
41
|
+
det.warnings.append("No composer.lock found — commit it for reproducible builds.")
|
|
42
|
+
|
|
43
|
+
packages = composer_packages(project)
|
|
44
|
+
m = re.search(r"(\d+)\.(\d+)", packages.get("php", ""))
|
|
45
|
+
low = f"{m.group(1)}.{m.group(2)}" if m else PHP_SUPPORTED[0]
|
|
46
|
+
det.matrix_versions = [v for v in PHP_SUPPORTED if _key(v) >= _key(low)] or [PHP_SUPPORTED[-1]]
|
|
47
|
+
if project.exists(".php-version"):
|
|
48
|
+
det.version_file = ".php-version"
|
|
49
|
+
det.version = project.read_text(".php-version").strip()
|
|
50
|
+
else:
|
|
51
|
+
det.version = det.matrix_versions[-1]
|
|
52
|
+
|
|
53
|
+
det.lint_tools = [tool for package, tool in _LINT_PACKAGES.items() if package in packages]
|
|
54
|
+
if "pestphp/pest" in packages:
|
|
55
|
+
det.test_runner = "pest"
|
|
56
|
+
elif "phpunit/phpunit" in packages or "symfony/phpunit-bridge" in packages:
|
|
57
|
+
det.test_runner = "phpunit"
|
|
58
|
+
else:
|
|
59
|
+
det.warnings.append("No test framework found (phpunit/phpunit or pestphp/pest) — the test job is disabled.")
|
|
60
|
+
env = project.read_text(".env") + project.read_text(".env.example")
|
|
61
|
+
implied = [ext for ext, pattern in _ENV_DATABASES.items() if re.search(pattern, env, re.M)]
|
|
62
|
+
det.services = detect_services([*packages, *implied], _SERVICE_PACKAGES)
|
|
63
|
+
return det
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def composer_packages(project: Project) -> Dict[str, str]:
|
|
67
|
+
"""`require` and `require-dev` of composer.json, merged."""
|
|
68
|
+
composer = project.read_json("composer.json")
|
|
69
|
+
if not isinstance(composer, dict):
|
|
70
|
+
return {}
|
|
71
|
+
return {**(composer.get("require") or {}), **(composer.get("require-dev") or {})}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _key(version: str) -> tuple:
|
|
75
|
+
return tuple(int(p) for p in version.split("."))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from quoll.languages.base import Detection, DockerDefaults, DockerImage
|
|
6
|
+
from quoll.languages.php.detect import composer_packages
|
|
7
|
+
from quoll.project import Project
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from quoll.docker.config import DockerConfig
|
|
11
|
+
|
|
12
|
+
# composer.json name -> install-php-extensions name, when they differ.
|
|
13
|
+
_EXTENSION_NAMES = {"zend-opcache": "opcache"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PhpImage(DockerImage):
|
|
17
|
+
"""FrankenPHP: PHP and its web server (Caddy) in one process, so there's no command to run."""
|
|
18
|
+
|
|
19
|
+
requires_command = False
|
|
20
|
+
|
|
21
|
+
def defaults(self, project: Project, det: Detection) -> DockerDefaults:
|
|
22
|
+
warnings = [] if project.is_dir("public") else ["No public/ directory: the whole project is served."]
|
|
23
|
+
return DockerDefaults(port=8080, warnings=warnings)
|
|
24
|
+
|
|
25
|
+
def writable_paths(self, cfg: DockerConfig) -> Optional[List[str]]:
|
|
26
|
+
# Caddy's certificates and configuration.
|
|
27
|
+
return ["/tmp", "/data/caddy", "/config/caddy"]
|
|
28
|
+
|
|
29
|
+
def context(self, project: Project, cfg: DockerConfig) -> Dict[str, Any]:
|
|
30
|
+
extensions = sorted(
|
|
31
|
+
_EXTENSION_NAMES.get(name[4:], name[4:]) for name in composer_packages(project) if name.startswith("ext-")
|
|
32
|
+
)
|
|
33
|
+
return {
|
|
34
|
+
"extensions": extensions,
|
|
35
|
+
"lockfile": project.exists("composer.lock"),
|
|
36
|
+
"public": project.is_dir("public"),
|
|
37
|
+
"env": self.env(cfg),
|
|
38
|
+
"post_build": self.post_build(cfg),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def env(self, cfg: DockerConfig) -> Dict[str, str]:
|
|
42
|
+
"""Environment of the build and the runtime."""
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
def post_build(self, cfg: DockerConfig) -> List[str]:
|
|
46
|
+
"""Commands run once dependencies and code are in place."""
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
def dockerignore(self, cfg: DockerConfig) -> List[str]:
|
|
50
|
+
return [
|
|
51
|
+
"vendor",
|
|
52
|
+
"node_modules",
|
|
53
|
+
"var",
|
|
54
|
+
".phpunit.cache",
|
|
55
|
+
".php-cs-fixer.cache",
|
|
56
|
+
".env",
|
|
57
|
+
".env.*",
|
|
58
|
+
"!.env.example",
|
|
59
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""PHP frameworks."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Laravel: tests run through artisan, which needs an .env and an application key."""
|
|
2
|
+
|
|
3
|
+
from quoll.languages.base import Framework
|
|
4
|
+
from quoll.languages.php.frameworks.laravel.ci import LaravelCI
|
|
5
|
+
from quoll.languages.php.frameworks.laravel.detect import detect
|
|
6
|
+
from quoll.languages.php.frameworks.laravel.docker import LaravelImage
|
|
7
|
+
from quoll.languages.php.frameworks.laravel.services import service_env
|
|
8
|
+
|
|
9
|
+
framework = Framework(
|
|
10
|
+
name="laravel",
|
|
11
|
+
language="php",
|
|
12
|
+
detect=detect,
|
|
13
|
+
ci=LaravelCI(),
|
|
14
|
+
test_runners=("artisan",),
|
|
15
|
+
docker=LaravelImage(),
|
|
16
|
+
service_env=service_env,
|
|
17
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, List
|
|
4
|
+
|
|
5
|
+
from quoll.ci.steps import Item, step
|
|
6
|
+
from quoll.languages.base import FrameworkCI
|
|
7
|
+
from quoll.languages.php.ci import report_flags
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from quoll.ci.config import CIConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LaravelCI(FrameworkCI):
|
|
14
|
+
def test(self, cfg: CIConfig) -> List[Item]:
|
|
15
|
+
t = cfg.test
|
|
16
|
+
cmd = "php artisan test"
|
|
17
|
+
if t.coverage:
|
|
18
|
+
cmd += f" --coverage --min={t.coverage_threshold} --coverage-clover coverage.xml"
|
|
19
|
+
cmd += report_flags(cfg)
|
|
20
|
+
return [
|
|
21
|
+
*cfg.language.ci.install(cfg),
|
|
22
|
+
step("Prepare environment", run="[ -f .env ] || cp .env.example .env\nphp artisan key:generate"),
|
|
23
|
+
step("Tests" + (" with coverage" if t.coverage else ""), run=cmd, secrets=t.env_secrets),
|
|
24
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from quoll.languages.base import Detection
|
|
4
|
+
from quoll.languages.php.detect import composer_packages
|
|
5
|
+
from quoll.project import Project
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect(project: Project, det: Detection) -> bool:
|
|
9
|
+
if "laravel/framework" not in composer_packages(project) or not project.exists("artisan"):
|
|
10
|
+
return False
|
|
11
|
+
# `php artisan test` drives PHPUnit or Pest, whichever the project uses.
|
|
12
|
+
det.test_runner = "artisan"
|
|
13
|
+
return True
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from quoll.languages.php.docker import PhpImage
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from quoll.docker.config import DockerConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LaravelImage(PhpImage):
|
|
12
|
+
def writable_paths(self, cfg: DockerConfig) -> Optional[List[str]]:
|
|
13
|
+
# The framework writes in storage/ and bootstrap/cache/, whose tree must stay: a writable root filesystem.
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
def env(self, cfg: DockerConfig) -> Dict[str, str]:
|
|
17
|
+
return {"APP_ENV": "production"}
|
|
18
|
+
|
|
19
|
+
def dockerignore(self, cfg: DockerConfig) -> List[str]:
|
|
20
|
+
return [
|
|
21
|
+
*super().dockerignore(cfg),
|
|
22
|
+
"storage/logs/*",
|
|
23
|
+
"storage/framework/cache/data/*",
|
|
24
|
+
"storage/framework/sessions/*",
|
|
25
|
+
"storage/framework/views/*",
|
|
26
|
+
"bootstrap/cache/*.php",
|
|
27
|
+
"public/hot",
|
|
28
|
+
"public/storage",
|
|
29
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from quoll.services import SERVICES
|
|
6
|
+
|
|
7
|
+
_CONNECTIONS = {"postgres": "pgsql", "mysql": "mysql"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def service_env(service: str, host: str, database: str) -> Dict[str, str]:
|
|
11
|
+
"""Laravel reads its database and Redis settings from DB_* and REDIS_* rather than URLs."""
|
|
12
|
+
if service in _CONNECTIONS:
|
|
13
|
+
user = "root" if service == "mysql" else "app"
|
|
14
|
+
password = "root" if service == "mysql" else "app"
|
|
15
|
+
return {
|
|
16
|
+
"DB_CONNECTION": _CONNECTIONS[service],
|
|
17
|
+
"DB_HOST": host,
|
|
18
|
+
"DB_PORT": str(SERVICES[service].port),
|
|
19
|
+
"DB_DATABASE": database,
|
|
20
|
+
"DB_USERNAME": user,
|
|
21
|
+
"DB_PASSWORD": password,
|
|
22
|
+
}
|
|
23
|
+
if service == "redis":
|
|
24
|
+
return {"REDIS_HOST": host}
|
|
25
|
+
return {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Symfony: the console's linters catch broken services, config and templates before the tests do."""
|
|
2
|
+
|
|
3
|
+
from quoll.languages.base import Framework
|
|
4
|
+
from quoll.languages.php.frameworks.symfony.ci import SymfonyCI
|
|
5
|
+
from quoll.languages.php.frameworks.symfony.detect import detect
|
|
6
|
+
from quoll.languages.php.frameworks.symfony.docker import SymfonyImage
|
|
7
|
+
|
|
8
|
+
framework = Framework(
|
|
9
|
+
name="symfony",
|
|
10
|
+
language="php",
|
|
11
|
+
detect=detect,
|
|
12
|
+
ci=SymfonyCI(),
|
|
13
|
+
lint_tools=("symfony-container", "symfony-yaml", "symfony-twig"),
|
|
14
|
+
docker=SymfonyImage(),
|
|
15
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, List
|
|
4
|
+
|
|
5
|
+
from quoll.ci.steps import Item, step
|
|
6
|
+
from quoll.languages.base import FrameworkCI
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from quoll.ci.config import CIConfig
|
|
10
|
+
|
|
11
|
+
# tool: (step name, command)
|
|
12
|
+
_LINT = {
|
|
13
|
+
"symfony-container": ("Lint service container", "php bin/console lint:container"),
|
|
14
|
+
"symfony-yaml": ("Lint YAML config", "php bin/console lint:yaml config --parse-tags"),
|
|
15
|
+
"symfony-twig": ("Lint Twig templates", "php bin/console lint:twig templates"),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SymfonyCI(FrameworkCI):
|
|
20
|
+
def lint(self, cfg: CIConfig, tool: str) -> List[Item]:
|
|
21
|
+
name, cmd = _LINT[tool]
|
|
22
|
+
return [step(name, run=cmd)]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from quoll.languages.base import Detection
|
|
4
|
+
from quoll.languages.php.detect import composer_packages
|
|
5
|
+
from quoll.project import Project
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect(project: Project, det: Detection) -> bool:
|
|
9
|
+
packages = composer_packages(project)
|
|
10
|
+
if "symfony/framework-bundle" not in packages or not project.exists("bin/console"):
|
|
11
|
+
return False
|
|
12
|
+
det.lint_tools.append("symfony-container")
|
|
13
|
+
if project.is_dir("config"):
|
|
14
|
+
det.lint_tools.append("symfony-yaml")
|
|
15
|
+
if "symfony/twig-bundle" in packages and project.is_dir("templates"):
|
|
16
|
+
det.lint_tools.append("symfony-twig")
|
|
17
|
+
return True
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from quoll.languages.php.docker import PhpImage
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from quoll.docker.config import DockerConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SymfonyImage(PhpImage):
|
|
12
|
+
def writable_paths(self, cfg: DockerConfig) -> Optional[List[str]]:
|
|
13
|
+
# The framework writes in var/, whose tree must stay: a writable root filesystem.
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
def env(self, cfg: DockerConfig) -> Dict[str, str]:
|
|
17
|
+
return {"APP_ENV": "prod", "APP_DEBUG": "0"}
|
|
18
|
+
|
|
19
|
+
def post_build(self, cfg: DockerConfig) -> List[str]:
|
|
20
|
+
# Flex's auto-scripts: cache:clear (warms the prod cache) and assets:install.
|
|
21
|
+
return ["composer run-script --no-dev post-install-cmd"]
|
|
22
|
+
|
|
23
|
+
def dockerignore(self, cfg: DockerConfig) -> List[str]:
|
|
24
|
+
# Symfony commits .env (defaults); secrets live in .env.local.
|
|
25
|
+
return ["vendor", "node_modules", "var", ".phpunit.cache", ".env.local", ".env.local.php", ".env.*.local"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{% block build %}
|
|
2
|
+
FROM python:{{ cfg.version }}-slim AS build
|
|
3
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
4
|
+
PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
5
|
+
WORKDIR /app
|
|
6
|
+
{% if cfg.package_manager == "uv" %}
|
|
7
|
+
COPY --from={{ uv_image }} /uv /uvx /bin/
|
|
8
|
+
ENV UV_COMPILE_BYTECODE=1 \
|
|
9
|
+
UV_LINK_MODE=copy \
|
|
10
|
+
UV_PYTHON_DOWNLOADS=0
|
|
11
|
+
# Dependencies first: code changes don't reinstall them.
|
|
12
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
13
|
+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
|
14
|
+
{% if lockfile %}
|
|
15
|
+
--mount=type=bind,source=uv.lock,target=uv.lock \
|
|
16
|
+
{% endif %}
|
|
17
|
+
uv sync {{ "--locked " if lockfile }}--no-dev --no-install-project
|
|
18
|
+
COPY . .
|
|
19
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
20
|
+
uv sync {{ "--locked " if lockfile }}--no-dev
|
|
21
|
+
{% elif cfg.package_manager == "poetry" %}
|
|
22
|
+
ENV POETRY_NO_INTERACTION=1 \
|
|
23
|
+
POETRY_VIRTUALENVS_IN_PROJECT=1
|
|
24
|
+
RUN --mount=type=cache,target=/root/.cache/pip pip install poetry
|
|
25
|
+
# Dependencies first: code changes don't reinstall them.
|
|
26
|
+
COPY pyproject.toml {{ "poetry.lock " if lockfile }}./
|
|
27
|
+
RUN --mount=type=cache,target=/root/.cache/pypoetry poetry install --only main --no-root
|
|
28
|
+
COPY . .
|
|
29
|
+
RUN --mount=type=cache,target=/root/.cache/pypoetry poetry install --only main
|
|
30
|
+
{% else %}
|
|
31
|
+
RUN python -m venv .venv
|
|
32
|
+
ENV PATH="/app/.venv/bin:$PATH"
|
|
33
|
+
{% if requirements %}
|
|
34
|
+
# Dependencies first: code changes don't reinstall them.
|
|
35
|
+
COPY requirements.txt .
|
|
36
|
+
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
|
|
37
|
+
{% endif %}
|
|
38
|
+
COPY . .
|
|
39
|
+
{% if installable %}
|
|
40
|
+
RUN --mount=type=cache,target=/root/.cache/pip pip install .
|
|
41
|
+
{% endif %}
|
|
42
|
+
{% endif %}
|
|
43
|
+
{% endblock %}
|
|
44
|
+
|
|
45
|
+
{% block runtime %}
|
|
46
|
+
FROM python:{{ cfg.version }}-slim
|
|
47
|
+
{{ os_updates }}
|
|
48
|
+
ENV PYTHONUNBUFFERED=1 \
|
|
49
|
+
PYTHONDONTWRITEBYTECODE=1 \
|
|
50
|
+
{% if src_layout %}
|
|
51
|
+
PYTHONPATH=/app/src \
|
|
52
|
+
{% endif %}
|
|
53
|
+
PATH="/app/.venv/bin:$PATH"
|
|
54
|
+
# A numeric user: Kubernetes can then check it isn't root (runAsNonRoot).
|
|
55
|
+
RUN groupadd --system --gid 10001 app && useradd --system --uid 10001 --gid app --no-create-home app
|
|
56
|
+
WORKDIR /app
|
|
57
|
+
COPY --from=build --chown=app:app /app /app
|
|
58
|
+
USER 10001:10001
|
|
59
|
+
EXPOSE {{ cfg.port }}
|
|
60
|
+
CMD {{ cfg.command | exec_form }}
|
|
61
|
+
{% endblock %}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Python: pip, uv and poetry projects."""
|
|
2
|
+
|
|
3
|
+
from quoll.languages.base import Language
|
|
4
|
+
from quoll.languages.python.ci import PythonCI
|
|
5
|
+
from quoll.languages.python.detect import detect
|
|
6
|
+
from quoll.languages.python.docker import PythonImage
|
|
7
|
+
from quoll.languages.python.frameworks import django, fastapi, flask
|
|
8
|
+
|
|
9
|
+
language = Language(
|
|
10
|
+
name="python",
|
|
11
|
+
markers=("pyproject.toml", "requirements.txt", "setup.py", "setup.cfg", "Pipfile"),
|
|
12
|
+
package_managers=("pip", "uv", "poetry"),
|
|
13
|
+
lint_tools=("ruff", "ruff-format", "flake8", "black", "mypy"),
|
|
14
|
+
test_runners=("pytest",),
|
|
15
|
+
detect=detect,
|
|
16
|
+
ci=PythonCI(),
|
|
17
|
+
frameworks=(django.framework, fastapi.framework, flask.framework),
|
|
18
|
+
docker=PythonImage(),
|
|
19
|
+
)
|