plain 0.69.0__tar.gz → 0.70.0__tar.gz
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.
- {plain-0.69.0 → plain-0.70.0}/PKG-INFO +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/AGENTS.md +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/CHANGELOG.md +11 -0
- {plain-0.69.0 → plain-0.70.0}/plain/assets/compile.py +20 -7
- {plain-0.69.0 → plain-0.70.0}/plain/assets/finders.py +15 -11
- {plain-0.69.0 → plain-0.70.0}/plain/assets/fingerprints.py +6 -5
- {plain-0.69.0 → plain-0.70.0}/plain/assets/urls.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/assets/views.py +23 -17
- {plain-0.69.0 → plain-0.70.0}/plain/chores/registry.py +14 -9
- {plain-0.69.0 → plain-0.70.0}/plain/cli/agent/__init__.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/cli/agent/docs.py +7 -6
- {plain-0.69.0 → plain-0.70.0}/plain/cli/agent/llmdocs.py +18 -8
- {plain-0.69.0 → plain-0.70.0}/plain/cli/agent/md.py +19 -14
- {plain-0.69.0 → plain-0.70.0}/plain/cli/agent/prompt.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/cli/agent/request.py +37 -17
- {plain-0.69.0 → plain-0.70.0}/plain/cli/build.py +2 -2
- {plain-0.69.0 → plain-0.70.0}/plain/cli/changelog.py +8 -4
- {plain-0.69.0 → plain-0.70.0}/plain/cli/chores.py +4 -4
- {plain-0.69.0 → plain-0.70.0}/plain/cli/core.py +8 -5
- {plain-0.69.0 → plain-0.70.0}/plain/cli/docs.py +2 -2
- {plain-0.69.0 → plain-0.70.0}/plain/cli/formatting.py +10 -7
- {plain-0.69.0 → plain-0.70.0}/plain/cli/output.py +6 -2
- {plain-0.69.0 → plain-0.70.0}/plain/cli/preflight.py +3 -3
- {plain-0.69.0 → plain-0.70.0}/plain/cli/print.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/cli/registry.py +10 -6
- {plain-0.69.0 → plain-0.70.0}/plain/cli/scaffold.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/cli/settings.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/cli/shell.py +10 -7
- {plain-0.69.0 → plain-0.70.0}/plain/cli/startup.py +3 -3
- {plain-0.69.0 → plain-0.70.0}/plain/cli/urls.py +10 -4
- {plain-0.69.0 → plain-0.70.0}/plain/cli/utils.py +2 -2
- {plain-0.69.0 → plain-0.70.0}/plain/csrf/middleware.py +15 -5
- {plain-0.69.0 → plain-0.70.0}/plain/csrf/views.py +11 -8
- {plain-0.69.0 → plain-0.70.0}/plain/debug.py +5 -2
- {plain-0.69.0 → plain-0.70.0}/plain/exceptions.py +19 -8
- {plain-0.69.0 → plain-0.70.0}/plain/forms/__init__.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/forms/boundfield.py +14 -7
- {plain-0.69.0 → plain-0.70.0}/plain/forms/exceptions.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/forms/fields.py +139 -97
- {plain-0.69.0 → plain-0.70.0}/plain/forms/forms.py +55 -39
- {plain-0.69.0 → plain-0.70.0}/plain/http/cookie.py +15 -7
- {plain-0.69.0 → plain-0.70.0}/plain/http/multipartparser.py +50 -30
- {plain-0.69.0 → plain-0.70.0}/plain/http/request.py +97 -73
- {plain-0.69.0 → plain-0.70.0}/plain/http/response.py +99 -80
- {plain-0.69.0 → plain-0.70.0}/plain/internal/__init__.py +8 -1
- {plain-0.69.0 → plain-0.70.0}/plain/internal/files/base.py +34 -18
- {plain-0.69.0 → plain-0.70.0}/plain/internal/files/locks.py +19 -11
- {plain-0.69.0 → plain-0.70.0}/plain/internal/files/move.py +8 -3
- {plain-0.69.0 → plain-0.70.0}/plain/internal/files/temp.py +23 -5
- {plain-0.69.0 → plain-0.70.0}/plain/internal/files/uploadedfile.py +42 -26
- {plain-0.69.0 → plain-0.70.0}/plain/internal/files/uploadhandler.py +48 -27
- {plain-0.69.0 → plain-0.70.0}/plain/internal/files/utils.py +13 -6
- {plain-0.69.0 → plain-0.70.0}/plain/internal/handlers/base.py +20 -6
- {plain-0.69.0 → plain-0.70.0}/plain/internal/handlers/exception.py +19 -5
- {plain-0.69.0 → plain-0.70.0}/plain/internal/handlers/wsgi.py +30 -18
- {plain-0.69.0 → plain-0.70.0}/plain/internal/middleware/headers.py +11 -2
- {plain-0.69.0 → plain-0.70.0}/plain/internal/middleware/hosts.py +10 -2
- {plain-0.69.0 → plain-0.70.0}/plain/internal/middleware/https.py +13 -3
- {plain-0.69.0 → plain-0.70.0}/plain/internal/middleware/slash.py +15 -5
- {plain-0.69.0 → plain-0.70.0}/plain/json.py +2 -1
- {plain-0.69.0 → plain-0.70.0}/plain/logs/configure.py +3 -1
- {plain-0.69.0 → plain-0.70.0}/plain/logs/debug.py +16 -5
- {plain-0.69.0 → plain-0.70.0}/plain/logs/formatters.py +6 -3
- {plain-0.69.0 → plain-0.70.0}/plain/logs/loggers.py +56 -52
- {plain-0.69.0 → plain-0.70.0}/plain/logs/utils.py +19 -9
- {plain-0.69.0 → plain-0.70.0}/plain/packages/config.py +14 -6
- {plain-0.69.0 → plain-0.70.0}/plain/packages/registry.py +27 -12
- {plain-0.69.0 → plain-0.70.0}/plain/paginator.py +31 -21
- {plain-0.69.0 → plain-0.70.0}/plain/preflight/checks.py +3 -1
- {plain-0.69.0 → plain-0.70.0}/plain/preflight/files.py +3 -1
- {plain-0.69.0 → plain-0.70.0}/plain/preflight/registry.py +25 -10
- {plain-0.69.0 → plain-0.70.0}/plain/preflight/results.py +10 -4
- {plain-0.69.0 → plain-0.70.0}/plain/preflight/security.py +7 -5
- {plain-0.69.0 → plain-0.70.0}/plain/preflight/urls.py +4 -1
- {plain-0.69.0 → plain-0.70.0}/plain/runtime/__init__.py +4 -3
- {plain-0.69.0 → plain-0.70.0}/plain/runtime/global_settings.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/runtime/user_settings.py +26 -17
- {plain-0.69.0 → plain-0.70.0}/plain/runtime/utils.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/signals/dispatch/dispatcher.py +39 -17
- {plain-0.69.0 → plain-0.70.0}/plain/signing.py +49 -30
- {plain-0.69.0 → plain-0.70.0}/plain/templates/jinja/__init__.py +13 -5
- {plain-0.69.0 → plain-0.70.0}/plain/templates/jinja/environments.py +4 -3
- {plain-0.69.0 → plain-0.70.0}/plain/templates/jinja/extensions.py +9 -3
- {plain-0.69.0 → plain-0.70.0}/plain/templates/jinja/filters.py +7 -2
- {plain-0.69.0 → plain-0.70.0}/plain/templates/jinja/globals.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/test/client.py +246 -174
- {plain-0.69.0 → plain-0.70.0}/plain/test/encoding.py +9 -6
- plain-0.70.0/plain/test/exceptions.py +15 -0
- {plain-0.69.0 → plain-0.70.0}/plain/urls/converters.py +13 -10
- {plain-0.69.0 → plain-0.70.0}/plain/urls/patterns.py +32 -20
- {plain-0.69.0 → plain-0.70.0}/plain/urls/resolvers.py +32 -22
- {plain-0.69.0 → plain-0.70.0}/plain/urls/utils.py +5 -1
- {plain-0.69.0 → plain-0.70.0}/plain/utils/cache.py +14 -8
- {plain-0.69.0 → plain-0.70.0}/plain/utils/crypto.py +21 -5
- {plain-0.69.0 → plain-0.70.0}/plain/utils/datastructures.py +84 -54
- {plain-0.69.0 → plain-0.70.0}/plain/utils/dateparse.py +10 -7
- {plain-0.69.0 → plain-0.70.0}/plain/utils/deconstruct.py +12 -4
- {plain-0.69.0 → plain-0.70.0}/plain/utils/decorators.py +5 -1
- {plain-0.69.0 → plain-0.70.0}/plain/utils/duration.py +8 -4
- {plain-0.69.0 → plain-0.70.0}/plain/utils/encoding.py +14 -7
- {plain-0.69.0 → plain-0.70.0}/plain/utils/functional.py +62 -47
- {plain-0.69.0 → plain-0.70.0}/plain/utils/hashable.py +5 -1
- {plain-0.69.0 → plain-0.70.0}/plain/utils/html.py +21 -14
- {plain-0.69.0 → plain-0.70.0}/plain/utils/http.py +16 -9
- {plain-0.69.0 → plain-0.70.0}/plain/utils/inspect.py +14 -6
- {plain-0.69.0 → plain-0.70.0}/plain/utils/ipv6.py +7 -3
- {plain-0.69.0 → plain-0.70.0}/plain/utils/itercompat.py +6 -1
- {plain-0.69.0 → plain-0.70.0}/plain/utils/module_loading.py +7 -3
- {plain-0.69.0 → plain-0.70.0}/plain/utils/regex_helper.py +23 -13
- {plain-0.69.0 → plain-0.70.0}/plain/utils/safestring.py +14 -6
- {plain-0.69.0 → plain-0.70.0}/plain/utils/text.py +34 -18
- {plain-0.69.0 → plain-0.70.0}/plain/utils/timezone.py +30 -19
- {plain-0.69.0 → plain-0.70.0}/plain/utils/tree.py +31 -18
- {plain-0.69.0 → plain-0.70.0}/plain/validators.py +71 -44
- {plain-0.69.0 → plain-0.70.0}/plain/views/base.py +16 -6
- {plain-0.69.0 → plain-0.70.0}/plain/views/errors.py +11 -4
- {plain-0.69.0 → plain-0.70.0}/plain/views/exceptions.py +4 -1
- {plain-0.69.0 → plain-0.70.0}/plain/views/objects.py +15 -15
- {plain-0.69.0 → plain-0.70.0}/plain/views/redirect.py +14 -10
- {plain-0.69.0 → plain-0.70.0}/plain/views/templates.py +1 -1
- {plain-0.69.0 → plain-0.70.0}/plain/wsgi.py +3 -1
- {plain-0.69.0 → plain-0.70.0}/pyproject.toml +1 -1
- plain-0.69.0/plain/test/exceptions.py +0 -7
- {plain-0.69.0 → plain-0.70.0}/.gitignore +0 -0
- {plain-0.69.0 → plain-0.70.0}/LICENSE +0 -0
- {plain-0.69.0 → plain-0.70.0}/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/__main__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/assets/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/assets/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/chores/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/chores/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/cli/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/cli/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/cli/install.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/cli/upgrade.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/csrf/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/forms/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/http/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/http/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/logs/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/logs/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/packages/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/packages/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/preflight/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/preflight/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/runtime/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/signals/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/signals/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/templates/AGENTS.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/templates/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/templates/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/templates/core.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/test/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/test/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/urls/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/urls/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/urls/exceptions.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/urls/routers.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/utils/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/utils/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/utils/timesince.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/views/README.md +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/views/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/plain/views/forms.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/.gitignore +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/app/.gitignore +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/app/settings.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/app/test/__init__.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/app/urls.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/conftest.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/test_cli.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/test_csrf.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/test_http_hosts.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/test_logs.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/test_runtime.py +0 -0
- {plain-0.69.0 → plain-0.70.0}/tests/test_wsgi.py +0 -0
@@ -10,7 +10,7 @@ The `plain` CLI is the main entrypoint for the framework. If `plain` is not avai
|
|
10
10
|
- `plain run <filename>`: Run a Python script with Plain configured.
|
11
11
|
- `plain agent docs <package>`: Show README.md and symbolicated source files for a specific package.
|
12
12
|
- `plain agent docs --list`: List packages with docs available.
|
13
|
-
- `plain agent request <path> --user <user_id>`: Make an authenticated request to the application and inspect the output.
|
13
|
+
- `plain agent request <path> --user <user_id>`: Make an authenticated request to the running application and inspect the output.
|
14
14
|
- `plain --help`: List all available commands (including those from installed packages).
|
15
15
|
|
16
16
|
## Code style
|
@@ -1,5 +1,16 @@
|
|
1
1
|
# plain changelog
|
2
2
|
|
3
|
+
## [0.70.0](https://github.com/dropseed/plain/releases/plain@0.70.0) (2025-09-30)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Added comprehensive type annotations throughout the codebase for improved IDE support and type checking ([365414c](https://github.com/dropseed/plain/commit/365414cc6f))
|
8
|
+
- The `Asset` class in `plain.assets.finders` is now a module-level public class instead of being defined inside `iter_assets()` ([6321765](https://github.com/dropseed/plain/commit/6321765d30))
|
9
|
+
|
10
|
+
### Upgrade instructions
|
11
|
+
|
12
|
+
- No changes required
|
13
|
+
|
3
14
|
## [0.69.0](https://github.com/dropseed/plain/releases/plain@0.69.0) (2025-09-29)
|
4
15
|
|
5
16
|
### What's changed
|
@@ -1,10 +1,14 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import gzip
|
2
4
|
import os
|
3
5
|
import shutil
|
6
|
+
from collections.abc import Iterator
|
7
|
+
from pathlib import Path
|
4
8
|
|
5
9
|
from plain.runtime import PLAIN_TEMP_PATH
|
6
10
|
|
7
|
-
from .finders import iter_assets
|
11
|
+
from .finders import Asset, iter_assets
|
8
12
|
from .fingerprints import AssetsFingerprintsManifest, get_file_fingerprint
|
9
13
|
|
10
14
|
SKIP_COMPRESS_EXTENSIONS = (
|
@@ -40,7 +44,7 @@ SKIP_COMPRESS_EXTENSIONS = (
|
|
40
44
|
)
|
41
45
|
|
42
46
|
|
43
|
-
def get_compiled_path():
|
47
|
+
def get_compiled_path() -> Path:
|
44
48
|
"""
|
45
49
|
Get the path at runtime to the compiled assets directory.
|
46
50
|
|
@@ -49,7 +53,9 @@ def get_compiled_path():
|
|
49
53
|
return PLAIN_TEMP_PATH / "assets" / "compiled"
|
50
54
|
|
51
55
|
|
52
|
-
def compile_assets(
|
56
|
+
def compile_assets(
|
57
|
+
*, target_dir: str, keep_original: bool, fingerprint: bool, compress: bool
|
58
|
+
) -> Iterator[tuple[str, str, list[str]]]:
|
53
59
|
"""
|
54
60
|
Compile all assets to the target directory and save a JSON manifest
|
55
61
|
mapping the original filenames to the compiled filenames.
|
@@ -73,17 +79,24 @@ def compile_assets(*, target_dir, keep_original, fingerprint, compress):
|
|
73
79
|
manifest.save()
|
74
80
|
|
75
81
|
|
76
|
-
def compile_asset(
|
82
|
+
def compile_asset(
|
83
|
+
*,
|
84
|
+
asset: Asset,
|
85
|
+
target_dir: str,
|
86
|
+
keep_original: bool,
|
87
|
+
fingerprint: bool,
|
88
|
+
compress: bool,
|
89
|
+
) -> tuple[str, list[str]]:
|
77
90
|
"""
|
78
91
|
Compile an asset to multiple output paths.
|
79
92
|
"""
|
80
|
-
compiled_paths = []
|
93
|
+
compiled_paths: list[str] = []
|
81
94
|
|
82
95
|
# The expected destination for the original asset
|
83
96
|
target_path = os.path.join(target_dir, asset.url_path)
|
84
97
|
|
85
98
|
# Keep track of where the final, resolved asset ends up
|
86
|
-
resolved_url_path = asset.url_path
|
99
|
+
resolved_url_path: str = asset.url_path
|
87
100
|
|
88
101
|
# Make sure all the expected directories exist
|
89
102
|
os.makedirs(os.path.dirname(target_path), exist_ok=True)
|
@@ -106,7 +119,7 @@ def compile_asset(*, asset, target_dir, keep_original, fingerprint, compress):
|
|
106
119
|
shutil.copy(asset.absolute_path, fingerprinted_path)
|
107
120
|
compiled_paths.append(fingerprinted_path)
|
108
121
|
|
109
|
-
resolved_url_path = os.path.relpath(fingerprinted_path, target_dir)
|
122
|
+
resolved_url_path = str(os.path.relpath(fingerprinted_path, target_dir))
|
110
123
|
|
111
124
|
if compress and extension.lower() not in SKIP_COMPRESS_EXTENSIONS:
|
112
125
|
for path in compiled_paths.copy():
|
@@ -1,4 +1,7 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import os
|
4
|
+
from collections.abc import Iterator
|
2
5
|
|
3
6
|
from plain.packages import packages_registry
|
4
7
|
from plain.runtime import APP_PATH
|
@@ -8,21 +11,22 @@ APP_ASSETS_DIR = APP_PATH / "assets"
|
|
8
11
|
SKIP_ASSETS = (".DS_Store", ".gitignore")
|
9
12
|
|
10
13
|
|
11
|
-
|
14
|
+
class Asset:
|
15
|
+
def __init__(self, *, url_path: str, absolute_path: str):
|
16
|
+
self.url_path = url_path
|
17
|
+
self.absolute_path = absolute_path
|
18
|
+
|
19
|
+
def __str__(self) -> str:
|
20
|
+
return self.url_path
|
21
|
+
|
22
|
+
|
23
|
+
def iter_assets() -> Iterator[Asset]:
|
12
24
|
"""
|
13
25
|
Iterate all valid asset files found in the installed
|
14
26
|
packages and the app itself.
|
15
27
|
"""
|
16
28
|
|
17
|
-
|
18
|
-
def __init__(self, *, url_path, absolute_path):
|
19
|
-
self.url_path = url_path
|
20
|
-
self.absolute_path = absolute_path
|
21
|
-
|
22
|
-
def __str__(self):
|
23
|
-
return self.url_path
|
24
|
-
|
25
|
-
def _iter_assets_dir(path):
|
29
|
+
def _iter_assets_dir(path: str) -> Iterator[tuple[str, str]]:
|
26
30
|
for root, _, files in os.walk(path):
|
27
31
|
for f in files:
|
28
32
|
if f in SKIP_ASSETS:
|
@@ -36,7 +40,7 @@ def iter_assets():
|
|
36
40
|
yield Asset(url_path=url_path, absolute_path=abs_path)
|
37
41
|
|
38
42
|
|
39
|
-
def iter_asset_dirs():
|
43
|
+
def iter_asset_dirs() -> Iterator[str]:
|
40
44
|
"""
|
41
45
|
Iterate all directories containing assets, from installed
|
42
46
|
packages and from app/assets.
|
@@ -15,18 +15,18 @@ class AssetsFingerprintsManifest(dict):
|
|
15
15
|
def __init__(self):
|
16
16
|
self.path = PLAIN_TEMP_PATH / "assets" / "fingerprints.json"
|
17
17
|
|
18
|
-
def load(self):
|
18
|
+
def load(self) -> None:
|
19
19
|
if self.path.exists():
|
20
20
|
with open(self.path) as f:
|
21
21
|
self.update(json.load(f))
|
22
22
|
|
23
|
-
def save(self):
|
23
|
+
def save(self) -> None:
|
24
24
|
with open(self.path, "w") as f:
|
25
25
|
json.dump(self, f, indent=2)
|
26
26
|
|
27
27
|
|
28
28
|
@cache
|
29
|
-
def _get_manifest():
|
29
|
+
def _get_manifest() -> AssetsFingerprintsManifest:
|
30
30
|
"""
|
31
31
|
A cached function for loading the asset fingerprints manifest,
|
32
32
|
so we don't have to keep loading it from disk over and over.
|
@@ -36,16 +36,17 @@ def _get_manifest():
|
|
36
36
|
return manifest
|
37
37
|
|
38
38
|
|
39
|
-
def get_fingerprinted_url_path(url_path):
|
39
|
+
def get_fingerprinted_url_path(url_path: str) -> str | None:
|
40
40
|
"""
|
41
41
|
Get the final fingerprinted path for an asset URL path.
|
42
42
|
"""
|
43
43
|
manifest = _get_manifest()
|
44
44
|
if url_path in manifest:
|
45
45
|
return manifest[url_path]
|
46
|
+
return None
|
46
47
|
|
47
48
|
|
48
|
-
def get_file_fingerprint(file_path):
|
49
|
+
def get_file_fingerprint(file_path: str) -> str:
|
49
50
|
"""
|
50
51
|
Get the fingerprint hash for a file.
|
51
52
|
"""
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import functools
|
2
4
|
import mimetypes
|
3
5
|
import os
|
@@ -28,14 +30,14 @@ class AssetView(View):
|
|
28
30
|
This class could be subclassed to further tweak the responses or behavior.
|
29
31
|
"""
|
30
32
|
|
31
|
-
def __init__(self, asset_path=None):
|
33
|
+
def __init__(self, asset_path: str | None = None):
|
32
34
|
# Allow a path to be passed in AssetView.as_view(path="...")
|
33
35
|
self.asset_path = asset_path
|
34
36
|
|
35
|
-
def get_url_path(self):
|
37
|
+
def get_url_path(self) -> str:
|
36
38
|
return self.asset_path or self.url_kwargs["path"]
|
37
39
|
|
38
|
-
def get(self):
|
40
|
+
def get(self) -> Response | FileResponse:
|
39
41
|
url_path = self.get_url_path()
|
40
42
|
|
41
43
|
# Make a trailing slash work, but we don't expect it
|
@@ -71,7 +73,7 @@ class AssetView(View):
|
|
71
73
|
response.headers = self.update_headers(response.headers, absolute_path)
|
72
74
|
return response
|
73
75
|
|
74
|
-
def get_asset_path(self, path):
|
76
|
+
def get_asset_path(self, path: str) -> str:
|
75
77
|
"""Get the path to the compiled asset"""
|
76
78
|
compiled_path = os.path.abspath(get_compiled_path())
|
77
79
|
asset_path = os.path.join(compiled_path, path)
|
@@ -82,13 +84,14 @@ class AssetView(View):
|
|
82
84
|
|
83
85
|
return asset_path
|
84
86
|
|
85
|
-
def get_debug_asset_path(self, path):
|
87
|
+
def get_debug_asset_path(self, path: str) -> str | None:
|
86
88
|
"""Make a "live" check to find the uncompiled asset in the filesystem"""
|
87
89
|
for asset in iter_assets():
|
88
90
|
if asset.url_path == path:
|
89
91
|
return asset.absolute_path
|
92
|
+
return None
|
90
93
|
|
91
|
-
def check_asset_path(self, path):
|
94
|
+
def check_asset_path(self, path: str | None) -> None:
|
92
95
|
if not path:
|
93
96
|
raise Http404("Asset not found")
|
94
97
|
|
@@ -99,7 +102,7 @@ class AssetView(View):
|
|
99
102
|
raise Http404("Asset is a directory")
|
100
103
|
|
101
104
|
@functools.cache
|
102
|
-
def get_last_modified(self, path):
|
105
|
+
def get_last_modified(self, path: str) -> str | None:
|
103
106
|
try:
|
104
107
|
mtime = os.path.getmtime(path)
|
105
108
|
except OSError:
|
@@ -107,9 +110,10 @@ class AssetView(View):
|
|
107
110
|
|
108
111
|
if mtime:
|
109
112
|
return formatdate(mtime, usegmt=True)
|
113
|
+
return None
|
110
114
|
|
111
115
|
@functools.cache
|
112
|
-
def get_etag(self, path):
|
116
|
+
def get_etag(self, path: str) -> str:
|
113
117
|
try:
|
114
118
|
mtime = os.path.getmtime(path)
|
115
119
|
except OSError:
|
@@ -120,10 +124,10 @@ class AssetView(View):
|
|
120
124
|
return f'"{timestamp:x}-{size:x}"'
|
121
125
|
|
122
126
|
@functools.cache
|
123
|
-
def get_size(self, path):
|
127
|
+
def get_size(self, path: str) -> int:
|
124
128
|
return os.path.getsize(path)
|
125
129
|
|
126
|
-
def update_headers(self, headers, path):
|
130
|
+
def update_headers(self, headers: dict, path: str) -> dict:
|
127
131
|
headers.setdefault("Access-Control-Allow-Origin", "*")
|
128
132
|
|
129
133
|
# Always vary on Accept-Encoding
|
@@ -165,7 +169,7 @@ class AssetView(View):
|
|
165
169
|
|
166
170
|
return headers
|
167
171
|
|
168
|
-
def is_immutable(self, path):
|
172
|
+
def is_immutable(self, path: str) -> bool:
|
169
173
|
"""
|
170
174
|
Determine whether an asset looks like it is immutable.
|
171
175
|
|
@@ -182,14 +186,14 @@ class AssetView(View):
|
|
182
186
|
|
183
187
|
return False
|
184
188
|
|
185
|
-
def get_encoded_path(self, path):
|
189
|
+
def get_encoded_path(self, path: str) -> str | None:
|
186
190
|
"""
|
187
191
|
If the client supports compression, return the path to the compressed file.
|
188
192
|
Otherwise, return the original path.
|
189
193
|
"""
|
190
194
|
accept_encoding = self.request.headers.get("Accept-Encoding")
|
191
195
|
if not accept_encoding:
|
192
|
-
return
|
196
|
+
return None
|
193
197
|
|
194
198
|
if "br" in accept_encoding:
|
195
199
|
br_path = path + ".br"
|
@@ -200,15 +204,16 @@ class AssetView(View):
|
|
200
204
|
gzip_path = path + ".gz"
|
201
205
|
if os.path.exists(gzip_path):
|
202
206
|
return gzip_path
|
207
|
+
return None
|
203
208
|
|
204
|
-
def get_redirect_response(self, path):
|
209
|
+
def get_redirect_response(self, path: str) -> ResponseRedirect | None:
|
205
210
|
"""If the asset is not found, try to redirect to the fingerprinted path"""
|
206
211
|
fingerprinted_url_path = get_fingerprinted_url_path(path)
|
207
212
|
|
208
213
|
if not fingerprinted_url_path or fingerprinted_url_path == path:
|
209
214
|
# Don't need to redirect if there is no fingerprinted path,
|
210
215
|
# or we're already looking at it.
|
211
|
-
return
|
216
|
+
return None
|
212
217
|
|
213
218
|
from .urls import AssetsRouter
|
214
219
|
|
@@ -221,7 +226,7 @@ class AssetView(View):
|
|
221
226
|
},
|
222
227
|
)
|
223
228
|
|
224
|
-
def get_conditional_response(self, path):
|
229
|
+
def get_conditional_response(self, path: str) -> ResponseNotModified | None:
|
225
230
|
"""
|
226
231
|
Support conditional requests (HTTP 304 response) based on ETag and Last-Modified headers.
|
227
232
|
"""
|
@@ -241,8 +246,9 @@ class AssetView(View):
|
|
241
246
|
response = ResponseNotModified()
|
242
247
|
response.headers = self.update_headers(response.headers, path)
|
243
248
|
return response
|
249
|
+
return None
|
244
250
|
|
245
|
-
def get_range_response(self, path):
|
251
|
+
def get_range_response(self, path: str) -> Response | StreamingResponse | None:
|
246
252
|
"""
|
247
253
|
Support range requests (HTTP 206 response).
|
248
254
|
"""
|
@@ -1,17 +1,22 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from types import FunctionType
|
4
|
+
from typing import Any
|
5
|
+
|
1
6
|
from plain.packages import packages_registry
|
2
7
|
|
3
8
|
|
4
9
|
class Chore:
|
5
|
-
def __init__(self, *, group, func):
|
10
|
+
def __init__(self, *, group: str, func: FunctionType):
|
6
11
|
self.group = group
|
7
12
|
self.func = func
|
8
13
|
self.name = f"{group}.{func.__name__}"
|
9
14
|
self.description = func.__doc__.strip() if func.__doc__ else ""
|
10
15
|
|
11
|
-
def __str__(self):
|
16
|
+
def __str__(self) -> str:
|
12
17
|
return self.name
|
13
18
|
|
14
|
-
def run(self):
|
19
|
+
def run(self) -> Any:
|
15
20
|
"""
|
16
21
|
Run the chore.
|
17
22
|
"""
|
@@ -20,21 +25,21 @@ class Chore:
|
|
20
25
|
|
21
26
|
class ChoresRegistry:
|
22
27
|
def __init__(self):
|
23
|
-
self._chores = {}
|
28
|
+
self._chores: dict[FunctionType, Chore] = {}
|
24
29
|
|
25
|
-
def register_chore(self, chore):
|
30
|
+
def register_chore(self, chore: Chore) -> None:
|
26
31
|
"""
|
27
32
|
Register a chore with the specified name.
|
28
33
|
"""
|
29
34
|
self._chores[chore.func] = chore
|
30
35
|
|
31
|
-
def import_modules(self):
|
36
|
+
def import_modules(self) -> None:
|
32
37
|
"""
|
33
38
|
Import modules from installed packages and app to trigger registration.
|
34
39
|
"""
|
35
40
|
packages_registry.autodiscover_modules("chores", include_app=True)
|
36
41
|
|
37
|
-
def get_chores(self):
|
42
|
+
def get_chores(self) -> list[Chore]:
|
38
43
|
"""
|
39
44
|
Get all registered chores.
|
40
45
|
"""
|
@@ -44,7 +49,7 @@ class ChoresRegistry:
|
|
44
49
|
chores_registry = ChoresRegistry()
|
45
50
|
|
46
51
|
|
47
|
-
def register_chore(group):
|
52
|
+
def register_chore(group: str) -> Any:
|
48
53
|
"""
|
49
54
|
Register a chore with a given group.
|
50
55
|
|
@@ -54,7 +59,7 @@ def register_chore(group):
|
|
54
59
|
pass
|
55
60
|
"""
|
56
61
|
|
57
|
-
def wrapper(func):
|
62
|
+
def wrapper(func: FunctionType) -> FunctionType:
|
58
63
|
chore = Chore(group=group, func=func)
|
59
64
|
chores_registry.register_chore(chore)
|
60
65
|
return func
|
@@ -7,7 +7,7 @@ from .request import request
|
|
7
7
|
|
8
8
|
@click.group("agent", invoke_without_command=True)
|
9
9
|
@click.pass_context
|
10
|
-
def agent(ctx):
|
10
|
+
def agent(ctx: click.Context) -> None:
|
11
11
|
"""Tools for coding agents."""
|
12
12
|
if ctx.invoked_subcommand is None:
|
13
13
|
# If no subcommand provided, show all AGENTS.md files
|
@@ -15,7 +15,7 @@ from .llmdocs import LLMDocs
|
|
15
15
|
is_flag=True,
|
16
16
|
help="List available packages",
|
17
17
|
)
|
18
|
-
def docs(package, show_list):
|
18
|
+
def docs(package: str, show_list: bool) -> None:
|
19
19
|
"""Show LLM-friendly documentation and source for a package."""
|
20
20
|
|
21
21
|
if show_list:
|
@@ -33,11 +33,12 @@ def docs(package, show_list):
|
|
33
33
|
available_packages.append("plain")
|
34
34
|
|
35
35
|
# Check other plain.* subpackages
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
if hasattr(plain, "__path__"):
|
37
|
+
for importer, modname, ispkg in pkgutil.iter_modules(
|
38
|
+
plain.__path__, "plain."
|
39
|
+
):
|
40
|
+
if ispkg:
|
41
|
+
available_packages.append(modname)
|
41
42
|
except Exception:
|
42
43
|
pass
|
43
44
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import ast
|
2
4
|
from pathlib import Path
|
3
5
|
|
@@ -7,10 +9,10 @@ import click
|
|
7
9
|
class LLMDocs:
|
8
10
|
"""Generates LLM-friendly documentation."""
|
9
11
|
|
10
|
-
def __init__(self, paths):
|
12
|
+
def __init__(self, paths: list[Path]):
|
11
13
|
self.paths = paths
|
12
14
|
|
13
|
-
def load(self):
|
15
|
+
def load(self) -> None:
|
14
16
|
self.docs = set()
|
15
17
|
self.sources = set()
|
16
18
|
|
@@ -47,7 +49,7 @@ class LLMDocs:
|
|
47
49
|
self.docs = sorted(self.docs)
|
48
50
|
self.sources = sorted(self.sources)
|
49
51
|
|
50
|
-
def display_path(self, path):
|
52
|
+
def display_path(self, path: Path) -> Path:
|
51
53
|
if "plain" in path.parts:
|
52
54
|
root_index = path.parts.index("plain")
|
53
55
|
elif "plainx" in path.parts:
|
@@ -58,7 +60,7 @@ class LLMDocs:
|
|
58
60
|
plain_root = Path(*path.parts[: root_index + 1])
|
59
61
|
return path.relative_to(plain_root.parent)
|
60
62
|
|
61
|
-
def print(self, relative_to=None):
|
63
|
+
def print(self, relative_to: Path | None = None) -> None:
|
62
64
|
for doc in self.docs:
|
63
65
|
if relative_to:
|
64
66
|
display_path = doc.relative_to(relative_to)
|
@@ -81,7 +83,7 @@ class LLMDocs:
|
|
81
83
|
click.echo()
|
82
84
|
|
83
85
|
@staticmethod
|
84
|
-
def symbolicate(file_path: Path):
|
86
|
+
def symbolicate(file_path: Path) -> str:
|
85
87
|
if "internal" in str(file_path).split("/"):
|
86
88
|
return ""
|
87
89
|
|
@@ -89,8 +91,16 @@ class LLMDocs:
|
|
89
91
|
|
90
92
|
parsed = ast.parse(source)
|
91
93
|
|
92
|
-
def should_skip(node):
|
93
|
-
if isinstance(node, ast.ClassDef
|
94
|
+
def should_skip(node: ast.AST) -> bool:
|
95
|
+
if isinstance(node, ast.ClassDef):
|
96
|
+
if any(
|
97
|
+
isinstance(d, ast.Name) and d.id == "internalcode"
|
98
|
+
for d in node.decorator_list
|
99
|
+
):
|
100
|
+
return True
|
101
|
+
if node.name.startswith("_"):
|
102
|
+
return True
|
103
|
+
elif isinstance(node, ast.FunctionDef):
|
94
104
|
if any(
|
95
105
|
isinstance(d, ast.Name) and d.id == "internalcode"
|
96
106
|
for d in node.decorator_list
|
@@ -104,7 +114,7 @@ class LLMDocs:
|
|
104
114
|
return True
|
105
115
|
return False
|
106
116
|
|
107
|
-
def process_node(node, indent=0):
|
117
|
+
def process_node(node: ast.AST, indent: int = 0) -> list[str]:
|
108
118
|
lines = []
|
109
119
|
prefix = " " * indent
|
110
120
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import importlib.util
|
2
4
|
import pkgutil
|
3
5
|
from pathlib import Path
|
@@ -7,7 +9,7 @@ import click
|
|
7
9
|
from ..output import iterate_markdown
|
8
10
|
|
9
11
|
|
10
|
-
def _get_packages_with_agents():
|
12
|
+
def _get_packages_with_agents() -> dict[str, Path]:
|
11
13
|
"""Get dict mapping package names to AGENTS.md paths."""
|
12
14
|
agents_files = {}
|
13
15
|
|
@@ -25,18 +27,21 @@ def _get_packages_with_agents():
|
|
25
27
|
agents_files["plain"] = agents_path
|
26
28
|
|
27
29
|
# Check other plain.* subpackages
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
30
|
+
if hasattr(plain, "__path__"):
|
31
|
+
for importer, modname, ispkg in pkgutil.iter_modules(
|
32
|
+
plain.__path__, "plain."
|
33
|
+
):
|
34
|
+
if ispkg:
|
35
|
+
try:
|
36
|
+
spec = importlib.util.find_spec(modname)
|
37
|
+
if spec and spec.origin:
|
38
|
+
package_path = Path(spec.origin).parent
|
39
|
+
# Look for AGENTS.md at package root
|
40
|
+
agents_path = package_path / "AGENTS.md"
|
41
|
+
if agents_path.exists():
|
42
|
+
agents_files[modname] = agents_path
|
43
|
+
except Exception:
|
44
|
+
continue
|
40
45
|
except Exception:
|
41
46
|
pass
|
42
47
|
|
@@ -57,7 +62,7 @@ def _get_packages_with_agents():
|
|
57
62
|
is_flag=True,
|
58
63
|
help="List packages with AGENTS.md files",
|
59
64
|
)
|
60
|
-
def md(package, show_all, show_list):
|
65
|
+
def md(package: str, show_all: bool, show_list: bool) -> None:
|
61
66
|
"""Show AGENTS.md for a package."""
|
62
67
|
|
63
68
|
agents_files = _get_packages_with_agents()
|