plain 0.50.0__py3-none-any.whl → 0.52.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.
plain/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.52.0](https://github.com/dropseed/plain/releases/plain@0.52.0) (2025-06-26)
4
+
5
+ ### What's changed
6
+
7
+ - Added `plain-changelog` as a standalone executable so you can view changelogs without importing the full framework ([e4e7324](https://github.com/dropseed/plain/commit/e4e7324cd284c800ff957933748d6639615cbea6))
8
+ - Removed the runtime dependency on the `packaging` library by replacing it with an internal version-comparison helper ([e4e7324](https://github.com/dropseed/plain/commit/e4e7324cd284c800ff957933748d6639615cbea6))
9
+ - Improved the error message when a package changelog cannot be found, now showing the path that was looked up ([f3c82bb](https://github.com/dropseed/plain/commit/f3c82bb59e07c1bddbdb2557f2043e039c1cd1e9))
10
+ - Fixed an f-string issue that broke `plain.debug.dd` on Python 3.11 ([ed24276](https://github.com/dropseed/plain/commit/ed24276a12191e4c8903369002dd32b69eb358b3))
11
+
12
+ ### Upgrade instructions
13
+
14
+ - No changes required
15
+
16
+ ## [0.51.0](https://github.com/dropseed/plain/releases/plain@0.51.0) (2025-06-24)
17
+
18
+ ### What's changed
19
+
20
+ - New `plain changelog` CLI sub-command to quickly view a package’s changelog from the terminal. Supports `--from`/`--to` flags to limit the version range ([50f0de7](https://github.com/dropseed/plain/commit/50f0de721f263ec6274852bd8838f4e5037b27dc)).
21
+
22
+ ### Upgrade instructions
23
+
24
+ - No changes required
25
+
3
26
  ## [0.50.0](https://github.com/dropseed/plain/releases/plain@0.50.0) (2025-06-23)
4
27
 
5
28
  ### What's changed
@@ -11,23 +34,25 @@
11
34
  ### Upgrade instructions
12
35
 
13
36
  - Update any scripts or documentation that call `plain urls …`:
14
- - Replace `plain urls --flat` with `plain urls list --flat`
37
+ - Replace `plain urls --flat` with `plain urls list --flat`
15
38
  - If you invoke preflight checks in CI or locally:
16
- - Replace `plain preflight --database <alias>` (or multiple aliases) with the new boolean flag: `plain preflight --database`
39
+ - Replace `plain preflight --database <alias>` (or multiple aliases) with the new boolean flag: `plain preflight --database`
17
40
  - In `settings.py` migrate to the new database configuration:
18
- ```python
19
- # Before
20
- DATABASES = {
21
- "default": {
22
- "ENGINE": "plain.backends.sqlite3",
23
- "NAME": BASE_DIR / "db.sqlite3",
24
- }
25
- }
26
-
27
- # After
28
- DATABASE = {
29
- "ENGINE": "plain.backends.sqlite3",
30
- "NAME": BASE_DIR / "db.sqlite3",
31
- }
32
- ```
33
- Remove any `DATABASES` and `DATABASE_ROUTERS` settings – they are no longer read.
41
+
42
+ ```python
43
+ # Before
44
+ DATABASES = {
45
+ "default": {
46
+ "ENGINE": "plain.backends.sqlite3",
47
+ "NAME": BASE_DIR / "db.sqlite3",
48
+ }
49
+ }
50
+
51
+ # After
52
+ DATABASE = {
53
+ "ENGINE": "plain.backends.sqlite3",
54
+ "NAME": BASE_DIR / "db.sqlite3",
55
+ }
56
+ ```
57
+
58
+ Remove any `DATABASES` and `DATABASE_ROUTERS` settings – they are no longer read.
plain/cli/changelog.py ADDED
@@ -0,0 +1,109 @@
1
+ import re
2
+ from importlib.util import find_spec
3
+ from pathlib import Path
4
+
5
+ import click
6
+
7
+ from .output import style_markdown
8
+
9
+
10
+ def parse_version(version_str):
11
+ """Parse a version string into a tuple of integers for comparison."""
12
+ # Remove 'v' prefix if present and split by dots
13
+ clean_version = version_str.lstrip("v")
14
+ parts = []
15
+ for part in clean_version.split("."):
16
+ # Extract numeric part from each segment
17
+ numeric_part = re.match(r"\d+", part)
18
+ if numeric_part:
19
+ parts.append(int(numeric_part.group()))
20
+ else:
21
+ parts.append(0)
22
+ return tuple(parts)
23
+
24
+
25
+ def compare_versions(v1, v2):
26
+ """Compare two version strings. Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2."""
27
+ parsed_v1 = parse_version(v1)
28
+ parsed_v2 = parse_version(v2)
29
+
30
+ # Pad shorter version with zeros
31
+ max_len = max(len(parsed_v1), len(parsed_v2))
32
+ parsed_v1 += (0,) * (max_len - len(parsed_v1))
33
+ parsed_v2 += (0,) * (max_len - len(parsed_v2))
34
+
35
+ if parsed_v1 < parsed_v2:
36
+ return -1
37
+ elif parsed_v1 > parsed_v2:
38
+ return 1
39
+ else:
40
+ return 0
41
+
42
+
43
+ @click.command("changelog")
44
+ @click.argument("package_label")
45
+ @click.option("--from", "from_version", help="Show entries from this version onwards")
46
+ @click.option("--to", "to_version", help="Show entries up to this version")
47
+ def changelog(package_label, from_version, to_version):
48
+ """Show changelog entries for a package."""
49
+ module_name = package_label.replace("-", ".")
50
+ spec = find_spec(module_name)
51
+ if not spec:
52
+ raise click.ClickException(f"Package {package_label} not found")
53
+
54
+ if spec.origin:
55
+ package_path = Path(spec.origin).resolve().parent
56
+ elif spec.submodule_search_locations:
57
+ package_path = Path(list(spec.submodule_search_locations)[0]).resolve()
58
+ else:
59
+ raise click.ClickException(f"Package {package_label} not found")
60
+
61
+ changelog_path = package_path / "CHANGELOG.md"
62
+ if not changelog_path.exists():
63
+ raise click.ClickException(
64
+ f"Changelog not found for {package_label} ({changelog_path})"
65
+ )
66
+
67
+ content = changelog_path.read_text()
68
+
69
+ entries = []
70
+ current_version = None
71
+ current_lines = []
72
+ version_re = re.compile(r"^## \[([^\]]+)\]")
73
+
74
+ for line in content.splitlines(keepends=True):
75
+ m = version_re.match(line)
76
+ if m:
77
+ if current_version is not None:
78
+ entries.append((current_version, current_lines))
79
+ current_version = m.group(1)
80
+ current_lines = [line]
81
+ else:
82
+ if current_version is not None:
83
+ current_lines.append(line)
84
+
85
+ if current_version is not None:
86
+ entries.append((current_version, current_lines))
87
+
88
+ def version_found(version):
89
+ return any(compare_versions(v, version) == 0 for v, _ in entries)
90
+
91
+ if from_version and not version_found(from_version):
92
+ click.secho(
93
+ f"Warning: version {from_version} not found in changelog",
94
+ fg="yellow",
95
+ err=True,
96
+ )
97
+
98
+ selected_lines = []
99
+ for version, lines in entries:
100
+ if from_version and compare_versions(version, from_version) <= 0:
101
+ continue
102
+ if to_version and compare_versions(version, to_version) > 0:
103
+ continue
104
+ selected_lines.extend(lines)
105
+
106
+ if not selected_lines:
107
+ return
108
+
109
+ click.echo(style_markdown("".join(selected_lines)))
plain/cli/core.py CHANGED
@@ -7,6 +7,7 @@ import plain.runtime
7
7
  from plain.exceptions import ImproperlyConfigured
8
8
 
9
9
  from .build import build
10
+ from .changelog import changelog
10
11
  from .chores import chores
11
12
  from .docs import docs
12
13
  from .formatting import PlainContext
@@ -32,6 +33,7 @@ plain_cli.add_command(chores)
32
33
  plain_cli.add_command(build)
33
34
  plain_cli.add_command(utils)
34
35
  plain_cli.add_command(urls)
36
+ plain_cli.add_command(changelog)
35
37
  plain_cli.add_command(setting)
36
38
  plain_cli.add_command(shell)
37
39
  plain_cli.add_command(run)
plain/cli/docs.py CHANGED
@@ -7,6 +7,8 @@ import click
7
7
 
8
8
  from plain.packages import packages_registry
9
9
 
10
+ from .output import iterate_markdown
11
+
10
12
 
11
13
  @click.command()
12
14
  @click.option("--llm", "llm", is_flag=True)
@@ -59,43 +61,7 @@ def docs(module, llm, open):
59
61
  if open:
60
62
  click.launch(str(readme_path))
61
63
  else:
62
-
63
- def _iterate_markdown(content):
64
- """
65
- Iterator that does basic markdown for a Click pager.
66
-
67
- Headings are yellow and bright, code blocks are indented.
68
- """
69
-
70
- in_code_block = False
71
- for line in content.splitlines():
72
- if line.startswith("```"):
73
- in_code_block = not in_code_block
74
-
75
- if in_code_block:
76
- yield click.style(line, dim=True)
77
- elif line.startswith("# "):
78
- yield click.style(line, fg="yellow", bold=True)
79
- elif line.startswith("## "):
80
- yield click.style(line, fg="yellow", bold=True)
81
- elif line.startswith("### "):
82
- yield click.style(line, fg="yellow", bold=True)
83
- elif line.startswith("#### "):
84
- yield click.style(line, fg="yellow", bold=True)
85
- elif line.startswith("##### "):
86
- yield click.style(line, fg="yellow", bold=True)
87
- elif line.startswith("###### "):
88
- yield click.style(line, fg="yellow", bold=True)
89
- elif line.startswith("**") and line.endswith("**"):
90
- yield click.style(line, bold=True)
91
- elif line.startswith("> "):
92
- yield click.style(line, italic=True)
93
- else:
94
- yield line
95
-
96
- yield "\n"
97
-
98
- click.echo_via_pager(_iterate_markdown(readme_path.read_text()))
64
+ click.echo_via_pager(iterate_markdown(readme_path.read_text()))
99
65
 
100
66
 
101
67
  class LLMDocs:
plain/cli/output.py ADDED
@@ -0,0 +1,41 @@
1
+ import click
2
+
3
+
4
+ def style_markdown(content):
5
+ return "".join(iterate_markdown(content))
6
+
7
+
8
+ def iterate_markdown(content):
9
+ """
10
+ Iterator that does basic markdown for a Click pager.
11
+
12
+ Headings are yellow and bright, code blocks are indented.
13
+ """
14
+
15
+ in_code_block = False
16
+ for line in content.splitlines():
17
+ if line.startswith("```"):
18
+ in_code_block = not in_code_block
19
+
20
+ if in_code_block:
21
+ yield click.style(line, dim=True)
22
+ elif line.startswith("# "):
23
+ yield click.style(line, fg="yellow", bold=True)
24
+ elif line.startswith("## "):
25
+ yield click.style(line, fg="yellow", bold=True)
26
+ elif line.startswith("### "):
27
+ yield click.style(line, fg="yellow", bold=True)
28
+ elif line.startswith("#### "):
29
+ yield click.style(line, fg="yellow", bold=True)
30
+ elif line.startswith("##### "):
31
+ yield click.style(line, fg="yellow", bold=True)
32
+ elif line.startswith("###### "):
33
+ yield click.style(line, fg="yellow", bold=True)
34
+ elif line.startswith("**") and line.endswith("**"):
35
+ yield click.style(line, bold=True)
36
+ elif line.startswith("> "):
37
+ yield click.style(line, italic=True)
38
+ else:
39
+ yield line
40
+
41
+ yield "\n"
plain/debug.py CHANGED
@@ -13,13 +13,14 @@ def dd(*objs):
13
13
  Dump the object and raise a ResponseException with the dump as the response content.
14
14
  """
15
15
 
16
- print(f"Dumping objects:\n{'\n'.join([pformat(obj) for obj in objs])}")
16
+ print_objs = "\n".join([pformat(obj) for obj in objs])
17
+ print(f"Dumping objects:\n{print_objs}")
17
18
 
18
- dump_strs = [
19
+ html_objs = [
19
20
  Markup("<pre><code>") + escape(pformat(obj)) + Markup("</code></pre>")
20
21
  for obj in objs
21
22
  ]
22
- combined_dump_str = Markup("\n\n").join(dump_strs)
23
+ combined_dump_str = Markup("\n\n").join(html_objs)
23
24
 
24
25
  response = Response()
25
26
  response.status_code = 500
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.50.0
3
+ Version: 0.52.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
@@ -1,7 +1,7 @@
1
- plain/CHANGELOG.md,sha256=Bl0GLzelbqK3Tyg-dAGBcHG49JwXtH3g6GHFCl-LEpk,1554
1
+ plain/CHANGELOG.md,sha256=G6FJgX85XVgzIKKIvEYLQ_f4Fzp6R0FsEesEpzgPGRE,2945
2
2
  plain/README.md,sha256=gik6DBZcJAITcm4WRq_L53AxkjY45eQLafyTCSf0CKE,3986
3
3
  plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
4
- plain/debug.py,sha256=abPkJY4aSbBYGEYSZST_ZY3ohXPGDdz9uWQBYRqfd3M,730
4
+ plain/debug.py,sha256=XdjnXcbPGsi0J2SpHGaLthhYU5AjhBlkHdemaP4sbYY,758
5
5
  plain/exceptions.py,sha256=Z9cbPE5im_Y-bjVq8cqC85gBoqOr80rLFG5wTKixrwE,5894
6
6
  plain/json.py,sha256=McJdsbMT1sYwkGRG--f2NSZz0hVXPMix9x3nKaaak2o,1262
7
7
  plain/paginator.py,sha256=iXiOyt2r_YwNrkqCRlaU7V-M_BKaaQ8XZElUBVa6yeU,5844
@@ -21,11 +21,13 @@ plain/chores/registry.py,sha256=V3WjuekRI22LFvJbqSkUXQtiOtuE2ZK8gKV1TRvxRUI,1866
21
21
  plain/cli/README.md,sha256=GzBry6mEilhM80SfVUg02ydGwAk0m-s6FAqQR1nRsMM,2022
22
22
  plain/cli/__init__.py,sha256=6w9T7K2WrPwh6DcaMb2oNt_CWU6Bc57nUTO2Bt1p38Y,63
23
23
  plain/cli/build.py,sha256=dKUYBNegvb6QNckR7XZ7CJJtINwZcmDvbUdv2dWwjf8,3226
24
+ plain/cli/changelog.py,sha256=j-k1yZk9mpm-fLZgeWastiyIisxNSuAJfXTQ2B6WQmk,3457
24
25
  plain/cli/chores.py,sha256=xXSSFvr8T5jWfLWqe6E8YVMw1BkQxyOHHVuY0x9RH0A,2412
25
- plain/cli/core.py,sha256=h8gjeBYQzYTzpmeDMAJTdVDOEoNCNcvnxml-KIsiRPo,2925
26
- plain/cli/docs.py,sha256=2CpTv5k-TNWf593tPiglvUVXWBGdfPmbGf8vl5AfJwU,8995
26
+ plain/cli/core.py,sha256=D3t83ujjjHayblM-RuttrGoNf8hMV9-l3zQsbhVAjWU,2991
27
+ plain/cli/docs.py,sha256=KCJCP_OVFb34zOkA6x7X6-iGFzx2tv4ZgXAM99TjWNg,7443
27
28
  plain/cli/formatting.py,sha256=1hZH13y1qwHcU2K2_Na388nw9uvoeQH8LrWL-O9h8Yc,2207
28
29
  plain/cli/help.py,sha256=otRSGxOJ5V8JMjpdZ8XYqUbdlYdJvxOMzQroLOWw-l0,801
30
+ plain/cli/output.py,sha256=Fe3xS6Va4Bi1ZNrqi0nh09THTsdCyMW2b9SPY5I4n-o,1318
29
31
  plain/cli/preflight.py,sha256=FWFwMZ0W_t8ObTTRMnBmaiGN8PqdEAWgmSEPGDwZFpA,4148
30
32
  plain/cli/print.py,sha256=XraUYrgODOJquIiEv78wSCYGRBplHXtXSS9QtFG5hqY,217
31
33
  plain/cli/registry.py,sha256=yKVMSDjW8g10nlV9sPXFGJQmhC_U-k4J4kM7N2OQVLA,1467
@@ -147,8 +149,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
147
149
  plain/views/objects.py,sha256=GGbcfg_9fPZ-PiaBwIHG2e__8GfWDR7JQtQ15wTyiHg,5970
148
150
  plain/views/redirect.py,sha256=daq2cQIkdDF78bt43sjuZxRAyJm_t_SKw6tyPmiXPIc,1985
149
151
  plain/views/templates.py,sha256=ivkI7LU7BXDQ0d4Geab96Is4-Cp03KbIntXRT1J8e6I,2139
150
- plain-0.50.0.dist-info/METADATA,sha256=GLcRDpS06XPtx52qLG5bIfrROE3g8CFffLNSz6Mdxt8,4297
151
- plain-0.50.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
152
- plain-0.50.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
153
- plain-0.50.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
154
- plain-0.50.0.dist-info/RECORD,,
152
+ plain-0.52.0.dist-info/METADATA,sha256=9Be3llzAmN2bZO2s_UPhej77hxsHZEntBmYbOarzbQw,4297
153
+ plain-0.52.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
154
+ plain-0.52.0.dist-info/entry_points.txt,sha256=nn4uKTRRZuEKOJv3810s3jtSMW0Gew7XDYiKIvBRR6M,93
155
+ plain-0.52.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
156
+ plain-0.52.0.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ plain = plain.cli.core:cli
3
+ plain-changelog = plain.cli.changelog:changelog
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- plain = plain.cli.core:cli
File without changes