plain 0.34.1__tar.gz → 0.36.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.
Files changed (162) hide show
  1. {plain-0.34.1 → plain-0.36.0}/PKG-INFO +1 -1
  2. {plain-0.34.1 → plain-0.36.0}/plain/assets/views.py +3 -3
  3. plain-0.36.0/plain/cli/build.py +107 -0
  4. plain-0.36.0/plain/cli/core.py +113 -0
  5. plain-0.36.0/plain/cli/docs.py +211 -0
  6. plain-0.36.0/plain/cli/preflight.py +127 -0
  7. plain-0.36.0/plain/cli/scaffold.py +53 -0
  8. plain-0.36.0/plain/cli/settings.py +60 -0
  9. plain-0.36.0/plain/cli/shell.py +56 -0
  10. plain-0.36.0/plain/cli/startup.py +45 -0
  11. plain-0.36.0/plain/cli/urls.py +87 -0
  12. plain-0.36.0/plain/cli/utils.py +15 -0
  13. {plain-0.34.1 → plain-0.36.0}/plain/csrf/README.md +1 -1
  14. {plain-0.34.1 → plain-0.36.0}/plain/csrf/middleware.py +3 -3
  15. {plain-0.34.1 → plain-0.36.0}/plain/http/README.md +1 -1
  16. {plain-0.34.1 → plain-0.36.0}/plain/http/response.py +15 -9
  17. {plain-0.34.1 → plain-0.36.0}/plain/runtime/global_settings.py +8 -1
  18. {plain-0.34.1 → plain-0.36.0}/plain/test/client.py +8 -8
  19. {plain-0.34.1 → plain-0.36.0}/plain/views/README.md +1 -1
  20. {plain-0.34.1 → plain-0.36.0}/plain/views/base.py +3 -2
  21. {plain-0.34.1 → plain-0.36.0}/plain/views/objects.py +3 -65
  22. {plain-0.34.1 → plain-0.36.0}/plain/views/redirect.py +5 -0
  23. {plain-0.34.1 → plain-0.36.0}/plain/views/templates.py +11 -9
  24. {plain-0.34.1 → plain-0.36.0}/pyproject.toml +1 -1
  25. plain-0.34.1/plain/cli/core.py +0 -781
  26. plain-0.34.1/plain/cli/startup.py +0 -33
  27. {plain-0.34.1 → plain-0.36.0}/.gitignore +0 -0
  28. {plain-0.34.1 → plain-0.36.0}/LICENSE +0 -0
  29. {plain-0.34.1 → plain-0.36.0}/README.md +0 -0
  30. {plain-0.34.1 → plain-0.36.0}/plain/README.md +0 -0
  31. {plain-0.34.1 → plain-0.36.0}/plain/__main__.py +0 -0
  32. {plain-0.34.1 → plain-0.36.0}/plain/assets/README.md +0 -0
  33. {plain-0.34.1 → plain-0.36.0}/plain/assets/__init__.py +0 -0
  34. {plain-0.34.1 → plain-0.36.0}/plain/assets/compile.py +0 -0
  35. {plain-0.34.1 → plain-0.36.0}/plain/assets/finders.py +0 -0
  36. {plain-0.34.1 → plain-0.36.0}/plain/assets/fingerprints.py +0 -0
  37. {plain-0.34.1 → plain-0.36.0}/plain/assets/urls.py +0 -0
  38. {plain-0.34.1 → plain-0.36.0}/plain/cli/README.md +0 -0
  39. {plain-0.34.1 → plain-0.36.0}/plain/cli/__init__.py +0 -0
  40. {plain-0.34.1 → plain-0.36.0}/plain/cli/formatting.py +0 -0
  41. {plain-0.34.1 → plain-0.36.0}/plain/cli/print.py +0 -0
  42. {plain-0.34.1 → plain-0.36.0}/plain/cli/registry.py +0 -0
  43. {plain-0.34.1 → plain-0.36.0}/plain/csrf/views.py +0 -0
  44. {plain-0.34.1 → plain-0.36.0}/plain/debug.py +0 -0
  45. {plain-0.34.1 → plain-0.36.0}/plain/exceptions.py +0 -0
  46. {plain-0.34.1 → plain-0.36.0}/plain/forms/README.md +0 -0
  47. {plain-0.34.1 → plain-0.36.0}/plain/forms/__init__.py +0 -0
  48. {plain-0.34.1 → plain-0.36.0}/plain/forms/boundfield.py +0 -0
  49. {plain-0.34.1 → plain-0.36.0}/plain/forms/exceptions.py +0 -0
  50. {plain-0.34.1 → plain-0.36.0}/plain/forms/fields.py +0 -0
  51. {plain-0.34.1 → plain-0.36.0}/plain/forms/forms.py +0 -0
  52. {plain-0.34.1 → plain-0.36.0}/plain/http/__init__.py +0 -0
  53. {plain-0.34.1 → plain-0.36.0}/plain/http/cookie.py +0 -0
  54. {plain-0.34.1 → plain-0.36.0}/plain/http/multipartparser.py +0 -0
  55. {plain-0.34.1 → plain-0.36.0}/plain/http/request.py +0 -0
  56. {plain-0.34.1 → plain-0.36.0}/plain/internal/__init__.py +0 -0
  57. {plain-0.34.1 → plain-0.36.0}/plain/internal/files/__init__.py +0 -0
  58. {plain-0.34.1 → plain-0.36.0}/plain/internal/files/base.py +0 -0
  59. {plain-0.34.1 → plain-0.36.0}/plain/internal/files/locks.py +0 -0
  60. {plain-0.34.1 → plain-0.36.0}/plain/internal/files/move.py +0 -0
  61. {plain-0.34.1 → plain-0.36.0}/plain/internal/files/temp.py +0 -0
  62. {plain-0.34.1 → plain-0.36.0}/plain/internal/files/uploadedfile.py +0 -0
  63. {plain-0.34.1 → plain-0.36.0}/plain/internal/files/uploadhandler.py +0 -0
  64. {plain-0.34.1 → plain-0.36.0}/plain/internal/files/utils.py +0 -0
  65. {plain-0.34.1 → plain-0.36.0}/plain/internal/handlers/__init__.py +0 -0
  66. {plain-0.34.1 → plain-0.36.0}/plain/internal/handlers/base.py +0 -0
  67. {plain-0.34.1 → plain-0.36.0}/plain/internal/handlers/exception.py +0 -0
  68. {plain-0.34.1 → plain-0.36.0}/plain/internal/handlers/wsgi.py +0 -0
  69. {plain-0.34.1 → plain-0.36.0}/plain/internal/middleware/__init__.py +0 -0
  70. {plain-0.34.1 → plain-0.36.0}/plain/internal/middleware/headers.py +0 -0
  71. {plain-0.34.1 → plain-0.36.0}/plain/internal/middleware/https.py +0 -0
  72. {plain-0.34.1 → plain-0.36.0}/plain/internal/middleware/slash.py +0 -0
  73. {plain-0.34.1 → plain-0.36.0}/plain/json.py +0 -0
  74. {plain-0.34.1 → plain-0.36.0}/plain/logs/README.md +0 -0
  75. {plain-0.34.1 → plain-0.36.0}/plain/logs/__init__.py +0 -0
  76. {plain-0.34.1 → plain-0.36.0}/plain/logs/configure.py +0 -0
  77. {plain-0.34.1 → plain-0.36.0}/plain/logs/loggers.py +0 -0
  78. {plain-0.34.1 → plain-0.36.0}/plain/logs/utils.py +0 -0
  79. {plain-0.34.1 → plain-0.36.0}/plain/packages/README.md +0 -0
  80. {plain-0.34.1 → plain-0.36.0}/plain/packages/__init__.py +0 -0
  81. {plain-0.34.1 → plain-0.36.0}/plain/packages/config.py +0 -0
  82. {plain-0.34.1 → plain-0.36.0}/plain/packages/registry.py +0 -0
  83. {plain-0.34.1 → plain-0.36.0}/plain/paginator.py +0 -0
  84. {plain-0.34.1 → plain-0.36.0}/plain/preflight/README.md +0 -0
  85. {plain-0.34.1 → plain-0.36.0}/plain/preflight/__init__.py +0 -0
  86. {plain-0.34.1 → plain-0.36.0}/plain/preflight/files.py +0 -0
  87. {plain-0.34.1 → plain-0.36.0}/plain/preflight/messages.py +0 -0
  88. {plain-0.34.1 → plain-0.36.0}/plain/preflight/registry.py +0 -0
  89. {plain-0.34.1 → plain-0.36.0}/plain/preflight/security.py +0 -0
  90. {plain-0.34.1 → plain-0.36.0}/plain/preflight/urls.py +0 -0
  91. {plain-0.34.1 → plain-0.36.0}/plain/runtime/README.md +0 -0
  92. {plain-0.34.1 → plain-0.36.0}/plain/runtime/__init__.py +0 -0
  93. {plain-0.34.1 → plain-0.36.0}/plain/runtime/user_settings.py +0 -0
  94. {plain-0.34.1 → plain-0.36.0}/plain/signals/README.md +0 -0
  95. {plain-0.34.1 → plain-0.36.0}/plain/signals/__init__.py +0 -0
  96. {plain-0.34.1 → plain-0.36.0}/plain/signals/dispatch/__init__.py +0 -0
  97. {plain-0.34.1 → plain-0.36.0}/plain/signals/dispatch/dispatcher.py +0 -0
  98. {plain-0.34.1 → plain-0.36.0}/plain/signals/dispatch/license.txt +0 -0
  99. {plain-0.34.1 → plain-0.36.0}/plain/signing.py +0 -0
  100. {plain-0.34.1 → plain-0.36.0}/plain/templates/README.md +0 -0
  101. {plain-0.34.1 → plain-0.36.0}/plain/templates/__init__.py +0 -0
  102. {plain-0.34.1 → plain-0.36.0}/plain/templates/core.py +0 -0
  103. {plain-0.34.1 → plain-0.36.0}/plain/templates/jinja/__init__.py +0 -0
  104. {plain-0.34.1 → plain-0.36.0}/plain/templates/jinja/environments.py +0 -0
  105. {plain-0.34.1 → plain-0.36.0}/plain/templates/jinja/extensions.py +0 -0
  106. {plain-0.34.1 → plain-0.36.0}/plain/templates/jinja/filters.py +0 -0
  107. {plain-0.34.1 → plain-0.36.0}/plain/templates/jinja/globals.py +0 -0
  108. {plain-0.34.1 → plain-0.36.0}/plain/test/README.md +0 -0
  109. {plain-0.34.1 → plain-0.36.0}/plain/test/__init__.py +0 -0
  110. {plain-0.34.1 → plain-0.36.0}/plain/test/encoding.py +0 -0
  111. {plain-0.34.1 → plain-0.36.0}/plain/test/exceptions.py +0 -0
  112. {plain-0.34.1 → plain-0.36.0}/plain/urls/README.md +0 -0
  113. {plain-0.34.1 → plain-0.36.0}/plain/urls/__init__.py +0 -0
  114. {plain-0.34.1 → plain-0.36.0}/plain/urls/converters.py +0 -0
  115. {plain-0.34.1 → plain-0.36.0}/plain/urls/exceptions.py +0 -0
  116. {plain-0.34.1 → plain-0.36.0}/plain/urls/patterns.py +0 -0
  117. {plain-0.34.1 → plain-0.36.0}/plain/urls/resolvers.py +0 -0
  118. {plain-0.34.1 → plain-0.36.0}/plain/urls/routers.py +0 -0
  119. {plain-0.34.1 → plain-0.36.0}/plain/urls/utils.py +0 -0
  120. {plain-0.34.1 → plain-0.36.0}/plain/utils/README.md +0 -0
  121. {plain-0.34.1 → plain-0.36.0}/plain/utils/__init__.py +0 -0
  122. {plain-0.34.1 → plain-0.36.0}/plain/utils/cache.py +0 -0
  123. {plain-0.34.1 → plain-0.36.0}/plain/utils/connection.py +0 -0
  124. {plain-0.34.1 → plain-0.36.0}/plain/utils/crypto.py +0 -0
  125. {plain-0.34.1 → plain-0.36.0}/plain/utils/datastructures.py +0 -0
  126. {plain-0.34.1 → plain-0.36.0}/plain/utils/dateparse.py +0 -0
  127. {plain-0.34.1 → plain-0.36.0}/plain/utils/deconstruct.py +0 -0
  128. {plain-0.34.1 → plain-0.36.0}/plain/utils/decorators.py +0 -0
  129. {plain-0.34.1 → plain-0.36.0}/plain/utils/duration.py +0 -0
  130. {plain-0.34.1 → plain-0.36.0}/plain/utils/encoding.py +0 -0
  131. {plain-0.34.1 → plain-0.36.0}/plain/utils/functional.py +0 -0
  132. {plain-0.34.1 → plain-0.36.0}/plain/utils/hashable.py +0 -0
  133. {plain-0.34.1 → plain-0.36.0}/plain/utils/html.py +0 -0
  134. {plain-0.34.1 → plain-0.36.0}/plain/utils/http.py +0 -0
  135. {plain-0.34.1 → plain-0.36.0}/plain/utils/inspect.py +0 -0
  136. {plain-0.34.1 → plain-0.36.0}/plain/utils/ipv6.py +0 -0
  137. {plain-0.34.1 → plain-0.36.0}/plain/utils/itercompat.py +0 -0
  138. {plain-0.34.1 → plain-0.36.0}/plain/utils/module_loading.py +0 -0
  139. {plain-0.34.1 → plain-0.36.0}/plain/utils/regex_helper.py +0 -0
  140. {plain-0.34.1 → plain-0.36.0}/plain/utils/safestring.py +0 -0
  141. {plain-0.34.1 → plain-0.36.0}/plain/utils/text.py +0 -0
  142. {plain-0.34.1 → plain-0.36.0}/plain/utils/timesince.py +0 -0
  143. {plain-0.34.1 → plain-0.36.0}/plain/utils/timezone.py +0 -0
  144. {plain-0.34.1 → plain-0.36.0}/plain/utils/tree.py +0 -0
  145. {plain-0.34.1 → plain-0.36.0}/plain/validators.py +0 -0
  146. {plain-0.34.1 → plain-0.36.0}/plain/views/__init__.py +0 -0
  147. {plain-0.34.1 → plain-0.36.0}/plain/views/csrf.py +0 -0
  148. {plain-0.34.1 → plain-0.36.0}/plain/views/errors.py +0 -0
  149. {plain-0.34.1 → plain-0.36.0}/plain/views/exceptions.py +0 -0
  150. {plain-0.34.1 → plain-0.36.0}/plain/views/forms.py +0 -0
  151. {plain-0.34.1 → plain-0.36.0}/plain/wsgi.py +0 -0
  152. {plain-0.34.1 → plain-0.36.0}/tests/.bolt/assets_collected/assets.json +0 -0
  153. {plain-0.34.1 → plain-0.36.0}/tests/.gitignore +0 -0
  154. {plain-0.34.1 → plain-0.36.0}/tests/app/.gitignore +0 -0
  155. {plain-0.34.1 → plain-0.36.0}/tests/app/settings.py +0 -0
  156. {plain-0.34.1 → plain-0.36.0}/tests/app/test/__init__.py +0 -0
  157. {plain-0.34.1 → plain-0.36.0}/tests/app/test/default_settings.py +0 -0
  158. {plain-0.34.1 → plain-0.36.0}/tests/app/urls.py +0 -0
  159. {plain-0.34.1 → plain-0.36.0}/tests/conftest.py +0 -0
  160. {plain-0.34.1 → plain-0.36.0}/tests/test_cli.py +0 -0
  161. {plain-0.34.1 → plain-0.36.0}/tests/test_runtime.py +0 -0
  162. {plain-0.34.1 → plain-0.36.0}/tests/test_wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.34.1
3
+ Version: 0.36.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -254,7 +254,7 @@ class AssetView(View):
254
254
 
255
255
  if not range_header.startswith("bytes="):
256
256
  return Response(
257
- status=416, headers=[("Content-Range", f"bytes */{file_size}")]
257
+ status_code=416, headers=[("Content-Range", f"bytes */{file_size}")]
258
258
  )
259
259
 
260
260
  range_values = range_header.split("=")[1].split("-")
@@ -263,7 +263,7 @@ class AssetView(View):
263
263
 
264
264
  if start >= file_size:
265
265
  return Response(
266
- status=416, headers=[("Content-Range", f"bytes */{file_size}")]
266
+ status_code=416, headers=[("Content-Range", f"bytes */{file_size}")]
267
267
  )
268
268
 
269
269
  end = min(end, file_size - 1)
@@ -272,7 +272,7 @@ class AssetView(View):
272
272
  f.seek(start)
273
273
  content = f.read(end - start + 1)
274
274
 
275
- response = StreamingResponse(BytesIO(content), status=206)
275
+ response = StreamingResponse(BytesIO(content), status_code=206)
276
276
  response.headers = self.update_headers(response.headers, path)
277
277
  response.headers["Content-Range"] = f"bytes {start}-{end}/{file_size}"
278
278
  response.headers["Content-Length"] = str(end - start + 1)
@@ -0,0 +1,107 @@
1
+ import shutil
2
+ import subprocess
3
+ import sys
4
+ import tomllib
5
+ from importlib.metadata import entry_points
6
+ from pathlib import Path
7
+
8
+ import click
9
+
10
+ import plain.runtime
11
+ from plain.assets.compile import compile_assets, get_compiled_path
12
+
13
+
14
+ @click.command()
15
+ @click.option(
16
+ "--keep-original/--no-keep-original",
17
+ "keep_original",
18
+ is_flag=True,
19
+ default=False,
20
+ help="Keep the original assets",
21
+ )
22
+ @click.option(
23
+ "--fingerprint/--no-fingerprint",
24
+ "fingerprint",
25
+ is_flag=True,
26
+ default=True,
27
+ help="Fingerprint the assets",
28
+ )
29
+ @click.option(
30
+ "--compress/--no-compress",
31
+ "compress",
32
+ is_flag=True,
33
+ default=True,
34
+ help="Compress the assets",
35
+ )
36
+ def build(keep_original, fingerprint, compress):
37
+ """Pre-deployment build step (compile assets, css, js, etc.)"""
38
+
39
+ if not keep_original and not fingerprint:
40
+ click.secho(
41
+ "You must either keep the original assets or fingerprint them.",
42
+ fg="red",
43
+ err=True,
44
+ )
45
+ sys.exit(1)
46
+
47
+ # Run user-defined build commands first
48
+ pyproject_path = plain.runtime.APP_PATH.parent / "pyproject.toml"
49
+ if pyproject_path.exists():
50
+ with pyproject_path.open("rb") as f:
51
+ pyproject = tomllib.load(f)
52
+
53
+ for name, data in (
54
+ pyproject.get("tool", {})
55
+ .get("plain", {})
56
+ .get("build", {})
57
+ .get("run", {})
58
+ .items()
59
+ ):
60
+ click.secho(f"Running {name} from pyproject.toml", bold=True)
61
+ result = subprocess.run(data["cmd"], shell=True)
62
+ print()
63
+ if result.returncode:
64
+ click.secho(f"Error in {name} (exit {result.returncode})", fg="red")
65
+ sys.exit(result.returncode)
66
+
67
+ # Then run installed package build steps (like tailwind, typically should run last...)
68
+ for entry_point in entry_points(group="plain.build"):
69
+ click.secho(f"Running {entry_point.name}", bold=True)
70
+ result = entry_point.load()()
71
+ print()
72
+
73
+ # Compile our assets
74
+ target_dir = get_compiled_path()
75
+ click.secho(f"Compiling assets to {target_dir}", bold=True)
76
+ if target_dir.exists():
77
+ click.secho("(clearing previously compiled assets)")
78
+ shutil.rmtree(target_dir)
79
+ target_dir.mkdir(parents=True, exist_ok=True)
80
+
81
+ total_files = 0
82
+ total_compiled = 0
83
+
84
+ for url_path, resolved_url_path, compiled_paths in compile_assets(
85
+ target_dir=target_dir,
86
+ keep_original=keep_original,
87
+ fingerprint=fingerprint,
88
+ compress=compress,
89
+ ):
90
+ if url_path == resolved_url_path:
91
+ click.secho(url_path, bold=True)
92
+ else:
93
+ click.secho(url_path, bold=True, nl=False)
94
+ click.secho(" → ", fg="yellow", nl=False)
95
+ click.echo(resolved_url_path)
96
+
97
+ print("\n".join(f" {Path(p).relative_to(Path.cwd())}" for p in compiled_paths))
98
+
99
+ total_files += 1
100
+ total_compiled += len(compiled_paths)
101
+
102
+ click.secho(
103
+ f"\nCompiled {total_files} assets into {total_compiled} files", fg="green"
104
+ )
105
+
106
+ # TODO could do a jinja pre-compile here too?
107
+ # environment.compile_templates() but it needs a target, ignore_errors=False
@@ -0,0 +1,113 @@
1
+ import traceback
2
+
3
+ import click
4
+ from click.core import Command, Context
5
+
6
+ import plain.runtime
7
+ from plain.exceptions import ImproperlyConfigured
8
+
9
+ from .build import build
10
+ from .docs import docs
11
+ from .formatting import PlainContext
12
+ from .preflight import preflight_checks
13
+ from .registry import cli_registry
14
+ from .scaffold import create
15
+ from .settings import setting
16
+ from .shell import run, shell
17
+ from .urls import urls
18
+ from .utils import utils
19
+
20
+
21
+ @click.group()
22
+ def plain_cli():
23
+ pass
24
+
25
+
26
+ plain_cli.add_command(docs)
27
+ plain_cli.add_command(preflight_checks)
28
+ plain_cli.add_command(create)
29
+ plain_cli.add_command(build)
30
+ plain_cli.add_command(utils)
31
+ plain_cli.add_command(urls)
32
+ plain_cli.add_command(setting)
33
+ plain_cli.add_command(shell)
34
+ plain_cli.add_command(run)
35
+
36
+
37
+ class CLIRegistryGroup(click.Group):
38
+ """
39
+ Click Group that exposes commands from the CLI registry.
40
+ """
41
+
42
+ def __init__(self, *args, **kwargs):
43
+ super().__init__(*args, **kwargs)
44
+ cli_registry.import_modules()
45
+
46
+ def list_commands(self, ctx):
47
+ return sorted(cli_registry.get_commands().keys())
48
+
49
+ def get_command(self, ctx, name):
50
+ commands = cli_registry.get_commands()
51
+ return commands.get(name)
52
+
53
+
54
+ class PlainCommandCollection(click.CommandCollection):
55
+ context_class = PlainContext
56
+
57
+ def __init__(self, *args, **kwargs):
58
+ sources = []
59
+
60
+ try:
61
+ plain.runtime.setup()
62
+
63
+ sources = [
64
+ CLIRegistryGroup(),
65
+ plain_cli,
66
+ ]
67
+ except plain.runtime.AppPathNotFound:
68
+ # Allow some commands to work regardless of being in a valid app
69
+ click.secho(
70
+ "Plain `app` directory not found. Some commands may be missing.",
71
+ fg="yellow",
72
+ err=True,
73
+ )
74
+
75
+ sources = [
76
+ plain_cli,
77
+ ]
78
+ except ImproperlyConfigured as e:
79
+ # Show what was configured incorrectly and exit
80
+ click.secho(
81
+ str(e),
82
+ fg="red",
83
+ err=True,
84
+ )
85
+
86
+ exit(1)
87
+ except Exception as e:
88
+ # Show the exception and exit
89
+ print("---")
90
+ print(traceback.format_exc())
91
+ print("---")
92
+
93
+ click.secho(
94
+ f"Error: {e}",
95
+ fg="red",
96
+ err=True,
97
+ )
98
+
99
+ exit(1)
100
+
101
+ super().__init__(*args, **kwargs)
102
+
103
+ self.sources = sources
104
+
105
+ def get_command(self, ctx: Context, cmd_name: str) -> Command | None:
106
+ cmd = super().get_command(ctx, cmd_name)
107
+ if cmd:
108
+ # Pass the formatting down to subcommands automatically
109
+ cmd.context_class = self.context_class
110
+ return cmd
111
+
112
+
113
+ cli = PlainCommandCollection()
@@ -0,0 +1,211 @@
1
+ import ast
2
+ import importlib.util
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from plain.packages import packages_registry
9
+
10
+
11
+ def symbolicate(file_path: Path):
12
+ if "internal" in str(file_path).split("/"):
13
+ return ""
14
+
15
+ source = file_path.read_text()
16
+
17
+ parsed = ast.parse(source)
18
+
19
+ def should_skip(node):
20
+ if isinstance(node, ast.ClassDef | ast.FunctionDef):
21
+ if any(
22
+ isinstance(d, ast.Name) and d.id == "internalcode"
23
+ for d in node.decorator_list
24
+ ):
25
+ return True
26
+ if node.name.startswith("_"): # and not node.name.endswith("__"):
27
+ return True
28
+ elif isinstance(node, ast.Assign):
29
+ for target in node.targets:
30
+ if (
31
+ isinstance(target, ast.Name) and target.id.startswith("_")
32
+ # and not target.id.endswith("__")
33
+ ):
34
+ return True
35
+ return False
36
+
37
+ def process_node(node, indent=0):
38
+ lines = []
39
+ prefix = " " * indent
40
+
41
+ if should_skip(node):
42
+ return []
43
+
44
+ if isinstance(node, ast.ClassDef):
45
+ decorators = [
46
+ f"{prefix}@{ast.unparse(d)}"
47
+ for d in node.decorator_list
48
+ if not (isinstance(d, ast.Name) and d.id == "internal")
49
+ ]
50
+ lines.extend(decorators)
51
+ bases = [ast.unparse(base) for base in node.bases]
52
+ lines.append(f"{prefix}class {node.name}({', '.join(bases)})")
53
+ # if ast.get_docstring(node):
54
+ # lines.append(f'{prefix} """{ast.get_docstring(node)}"""')
55
+ for child in node.body:
56
+ child_lines = process_node(child, indent + 1)
57
+ if child_lines:
58
+ lines.extend(child_lines)
59
+ # if not has_body:
60
+ # lines.append(f"{prefix} pass")
61
+
62
+ elif isinstance(node, ast.FunctionDef):
63
+ decorators = [f"{prefix}@{ast.unparse(d)}" for d in node.decorator_list]
64
+ lines.extend(decorators)
65
+ args = ast.unparse(node.args)
66
+ lines.append(f"{prefix}def {node.name}({args})")
67
+ # if ast.get_docstring(node):
68
+ # lines.append(f'{prefix} """{ast.get_docstring(node)}"""')
69
+ # lines.append(f"{prefix} pass")
70
+
71
+ elif isinstance(node, ast.Assign):
72
+ for target in node.targets:
73
+ if isinstance(target, ast.Name):
74
+ lines.append(f"{prefix}{target.id} = {ast.unparse(node.value)}")
75
+
76
+ return lines
77
+
78
+ symbolicated_lines = []
79
+ for node in parsed.body:
80
+ symbolicated_lines.extend(process_node(node))
81
+
82
+ return "\n".join(symbolicated_lines)
83
+
84
+
85
+ @click.command()
86
+ @click.option("--llm", "llm", is_flag=True)
87
+ @click.option("--open")
88
+ @click.argument("module", default="")
89
+ def docs(module, llm, open):
90
+ if not module and not llm:
91
+ click.secho("You must specify a module or use --llm", fg="red")
92
+ sys.exit(1)
93
+
94
+ if llm:
95
+ click.echo(
96
+ "Below is all of the documentation and abbreviated source code for the Plain web framework. "
97
+ "Your job is to read and understand it, and then act as the Plain Framework Assistant and "
98
+ "help the developer accomplish whatever they want to do next."
99
+ "\n\n---\n\n"
100
+ )
101
+
102
+ docs = set()
103
+ sources = set()
104
+
105
+ # Get everything for Plain core
106
+ for path in Path(__file__).parent.parent.glob("**/*.md"):
107
+ docs.add(path)
108
+ for source in Path(__file__).parent.parent.glob("**/*.py"):
109
+ sources.add(source)
110
+
111
+ # Find every *.md file in the other plain packages and installed apps
112
+ for package_config in packages_registry.get_package_configs():
113
+ if package_config.name.startswith("app."):
114
+ # Ignore app packages for now
115
+ continue
116
+
117
+ for path in Path(package_config.path).glob("**/*.md"):
118
+ docs.add(path)
119
+
120
+ for source in Path(package_config.path).glob("**/*.py"):
121
+ sources.add(source)
122
+
123
+ docs = sorted(docs)
124
+ sources = sorted(sources)
125
+
126
+ for doc in docs:
127
+ try:
128
+ display_path = doc.relative_to(Path.cwd())
129
+ except ValueError:
130
+ display_path = doc.absolute()
131
+ click.secho(f"<Docs: {display_path}>", fg="yellow")
132
+ click.echo(doc.read_text())
133
+ click.secho(f"</Docs: {display_path}>", fg="yellow")
134
+ click.echo()
135
+
136
+ for source in sources:
137
+ if symbolicated := symbolicate(source):
138
+ try:
139
+ display_path = source.relative_to(Path.cwd())
140
+ except ValueError:
141
+ display_path = source.absolute()
142
+ click.secho(f"<Source: {display_path}>", fg="yellow")
143
+ click.echo(symbolicated)
144
+ click.secho(f"</Source: {display_path}>", fg="yellow")
145
+ click.echo()
146
+
147
+ click.secho(
148
+ "That's everything! Copy this into your AI tool of choice.",
149
+ err=True,
150
+ fg="green",
151
+ )
152
+
153
+ return
154
+
155
+ if module:
156
+ # Automatically prefix if we need to
157
+ if not module.startswith("plain"):
158
+ module = f"plain.{module}"
159
+
160
+ # Get the README.md file for the module
161
+ spec = importlib.util.find_spec(module)
162
+ if not spec:
163
+ click.secho(f"Module {module} not found", fg="red")
164
+ sys.exit(1)
165
+
166
+ module_path = Path(spec.origin).parent
167
+ readme_path = module_path / "README.md"
168
+ if not readme_path.exists():
169
+ click.secho(f"README.md not found for {module}", fg="red")
170
+ sys.exit(1)
171
+
172
+ if open:
173
+ click.launch(str(readme_path))
174
+ else:
175
+
176
+ def _iterate_markdown(content):
177
+ """
178
+ Iterator that does basic markdown for a Click pager.
179
+
180
+ Headings are yellow and bright, code blocks are indented.
181
+ """
182
+
183
+ in_code_block = False
184
+ for line in content.splitlines():
185
+ if line.startswith("```"):
186
+ in_code_block = not in_code_block
187
+
188
+ if in_code_block:
189
+ yield click.style(line, dim=True)
190
+ elif line.startswith("# "):
191
+ yield click.style(line, fg="yellow", bold=True)
192
+ elif line.startswith("## "):
193
+ yield click.style(line, fg="yellow", bold=True)
194
+ elif line.startswith("### "):
195
+ yield click.style(line, fg="yellow", bold=True)
196
+ elif line.startswith("#### "):
197
+ yield click.style(line, fg="yellow", bold=True)
198
+ elif line.startswith("##### "):
199
+ yield click.style(line, fg="yellow", bold=True)
200
+ elif line.startswith("###### "):
201
+ yield click.style(line, fg="yellow", bold=True)
202
+ elif line.startswith("**") and line.endswith("**"):
203
+ yield click.style(line, bold=True)
204
+ elif line.startswith("> "):
205
+ yield click.style(line, italic=True)
206
+ else:
207
+ yield line
208
+
209
+ yield "\n"
210
+
211
+ click.echo_via_pager(_iterate_markdown(readme_path.read_text()))
@@ -0,0 +1,127 @@
1
+ import click
2
+
3
+ from plain import preflight
4
+ from plain.packages import packages_registry
5
+
6
+
7
+ @click.command("preflight")
8
+ @click.argument("package_label", nargs=-1)
9
+ @click.option(
10
+ "--deploy",
11
+ is_flag=True,
12
+ help="Check deployment settings.",
13
+ )
14
+ @click.option(
15
+ "--fail-level",
16
+ default="ERROR",
17
+ type=click.Choice(["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]),
18
+ help="Message level that will cause the command to exit with a non-zero status. Default is ERROR.",
19
+ )
20
+ @click.option(
21
+ "--database",
22
+ "databases",
23
+ multiple=True,
24
+ help="Run database related checks against these aliases.",
25
+ )
26
+ def preflight_checks(package_label, deploy, fail_level, databases):
27
+ """
28
+ Use the system check framework to validate entire Plain project.
29
+ Raise CommandError for any serious message (error or critical errors).
30
+ If there are only light messages (like warnings), print them to stderr
31
+ and don't raise an exception.
32
+ """
33
+ include_deployment_checks = deploy
34
+
35
+ if package_label:
36
+ package_configs = [
37
+ packages_registry.get_package_config(label) for label in package_label
38
+ ]
39
+ else:
40
+ package_configs = None
41
+
42
+ all_issues = preflight.run_checks(
43
+ package_configs=package_configs,
44
+ include_deployment_checks=include_deployment_checks,
45
+ databases=databases,
46
+ )
47
+
48
+ header, body, footer = "", "", ""
49
+ visible_issue_count = 0 # excludes silenced warnings
50
+
51
+ if all_issues:
52
+ debugs = [
53
+ e for e in all_issues if e.level < preflight.INFO and not e.is_silenced()
54
+ ]
55
+ infos = [
56
+ e
57
+ for e in all_issues
58
+ if preflight.INFO <= e.level < preflight.WARNING and not e.is_silenced()
59
+ ]
60
+ warnings = [
61
+ e
62
+ for e in all_issues
63
+ if preflight.WARNING <= e.level < preflight.ERROR and not e.is_silenced()
64
+ ]
65
+ errors = [
66
+ e
67
+ for e in all_issues
68
+ if preflight.ERROR <= e.level < preflight.CRITICAL and not e.is_silenced()
69
+ ]
70
+ criticals = [
71
+ e
72
+ for e in all_issues
73
+ if preflight.CRITICAL <= e.level and not e.is_silenced()
74
+ ]
75
+ sorted_issues = [
76
+ (criticals, "CRITICALS"),
77
+ (errors, "ERRORS"),
78
+ (warnings, "WARNINGS"),
79
+ (infos, "INFOS"),
80
+ (debugs, "DEBUGS"),
81
+ ]
82
+
83
+ for issues, group_name in sorted_issues:
84
+ if issues:
85
+ visible_issue_count += len(issues)
86
+ formatted = (
87
+ click.style(str(e), fg="red")
88
+ if e.is_serious()
89
+ else click.style(str(e), fg="yellow")
90
+ for e in issues
91
+ )
92
+ formatted = "\n".join(sorted(formatted))
93
+ body += f"\n{group_name}:\n{formatted}\n"
94
+
95
+ if visible_issue_count:
96
+ header = "Preflight check identified some issues:\n"
97
+
98
+ if any(
99
+ e.is_serious(getattr(preflight, fail_level)) and not e.is_silenced()
100
+ for e in all_issues
101
+ ):
102
+ footer += "\n"
103
+ footer += "Preflight check identified {} ({} silenced).".format(
104
+ "no issues"
105
+ if visible_issue_count == 0
106
+ else "1 issue"
107
+ if visible_issue_count == 1
108
+ else f"{visible_issue_count} issues",
109
+ len(all_issues) - visible_issue_count,
110
+ )
111
+ msg = click.style(f"SystemCheckError: {header}", fg="red") + body + footer
112
+ raise click.ClickException(msg)
113
+ else:
114
+ if visible_issue_count:
115
+ footer += "\n"
116
+ footer += "Preflight check identified {} ({} silenced).".format(
117
+ "no issues"
118
+ if visible_issue_count == 0
119
+ else "1 issue"
120
+ if visible_issue_count == 1
121
+ else f"{visible_issue_count} issues",
122
+ len(all_issues) - visible_issue_count,
123
+ )
124
+ msg = header + body + footer
125
+ click.echo(msg, err=True)
126
+ else:
127
+ click.secho("✔ Preflight check identified no issues.", err=True, fg="green")
@@ -0,0 +1,53 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+
5
+ import plain.runtime
6
+
7
+
8
+ @click.command()
9
+ @click.argument("package_name")
10
+ def create(package_name):
11
+ """
12
+ Create a new local package.
13
+
14
+ The PACKAGE_NAME is typically a plural noun, like "users" or "posts",
15
+ where you might create a "User" or "Post" model inside of the package.
16
+ """
17
+ package_dir = plain.runtime.APP_PATH / package_name
18
+ package_dir.mkdir(exist_ok=True)
19
+
20
+ empty_dirs = (
21
+ f"templates/{package_name}",
22
+ "migrations",
23
+ )
24
+ for d in empty_dirs:
25
+ (package_dir / d).mkdir(parents=True, exist_ok=True)
26
+
27
+ empty_files = (
28
+ "__init__.py",
29
+ "migrations/__init__.py",
30
+ "models.py",
31
+ "views.py",
32
+ )
33
+ for f in empty_files:
34
+ (package_dir / f).touch(exist_ok=True)
35
+
36
+ # Create a urls.py file with a default namespace
37
+ if not (package_dir / "urls.py").exists():
38
+ (package_dir / "urls.py").write_text(
39
+ f"""from plain.urls import path, Router
40
+
41
+
42
+ class {package_name.capitalize()}Router(Router):
43
+ namespace = f"{package_name}"
44
+ urls = [
45
+ # path("", views.IndexView, name="index"),
46
+ ]
47
+ """
48
+ )
49
+
50
+ click.secho(
51
+ f'Created {package_dir.relative_to(Path.cwd())}. Make sure to add "{package_name}" to INSTALLED_PACKAGES!',
52
+ fg="green",
53
+ )