plain 0.66.0__py3-none-any.whl → 0.101.2__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 +684 -0
- plain/README.md +1 -1
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -53
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +112 -28
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +175 -102
- plain/cli/print.py +4 -4
- plain/cli/registry.py +95 -26
- plain/cli/request.py +206 -0
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -13
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +40 -15
- plain/paginator.py +31 -21
- plain/preflight/README.md +208 -23
- plain/preflight/__init__.py +5 -24
- plain/preflight/checks.py +12 -0
- plain/preflight/files.py +19 -13
- plain/preflight/registry.py +80 -58
- plain/preflight/results.py +37 -0
- plain/preflight/security.py +65 -71
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +10 -48
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +43 -33
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +14 -27
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +56 -40
- plain/urls/resolvers.py +38 -28
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/cli/agent/request.py +0 -181
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/preflight/messages.py +0 -81
- plain/templates/AGENTS.md +0 -3
- plain-0.66.0.dist-info/RECORD +0 -168
- plain-0.66.0.dist-info/entry_points.txt +0 -4
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/cli/agent/docs.py
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import importlib.util
|
|
2
|
-
import pkgutil
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
|
|
7
|
-
from .llmdocs import LLMDocs
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@click.command()
|
|
11
|
-
@click.argument("package", default="", required=False)
|
|
12
|
-
@click.option(
|
|
13
|
-
"--list",
|
|
14
|
-
"show_list",
|
|
15
|
-
is_flag=True,
|
|
16
|
-
help="List available packages",
|
|
17
|
-
)
|
|
18
|
-
def docs(package, show_list):
|
|
19
|
-
"""Show LLM-friendly documentation and source for a package."""
|
|
20
|
-
|
|
21
|
-
if show_list:
|
|
22
|
-
# List available packages using same discovery logic as md command
|
|
23
|
-
try:
|
|
24
|
-
available_packages = []
|
|
25
|
-
|
|
26
|
-
# Check for plain.* subpackages (including core plain)
|
|
27
|
-
try:
|
|
28
|
-
import plain
|
|
29
|
-
|
|
30
|
-
# Check core plain package (namespace package)
|
|
31
|
-
plain_spec = importlib.util.find_spec("plain")
|
|
32
|
-
if plain_spec and plain_spec.submodule_search_locations:
|
|
33
|
-
available_packages.append("plain")
|
|
34
|
-
|
|
35
|
-
# Check other plain.* subpackages
|
|
36
|
-
for importer, modname, ispkg in pkgutil.iter_modules(
|
|
37
|
-
plain.__path__, "plain."
|
|
38
|
-
):
|
|
39
|
-
if ispkg:
|
|
40
|
-
available_packages.append(modname)
|
|
41
|
-
except Exception:
|
|
42
|
-
pass
|
|
43
|
-
|
|
44
|
-
if available_packages:
|
|
45
|
-
for pkg in sorted(available_packages):
|
|
46
|
-
click.echo(f"- {pkg}")
|
|
47
|
-
else:
|
|
48
|
-
click.echo("No packages found.")
|
|
49
|
-
except Exception as e:
|
|
50
|
-
click.echo(f"Error listing packages: {e}")
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
if not package:
|
|
54
|
-
raise click.UsageError(
|
|
55
|
-
"Package name required. Usage: plain agent docs [package-name]"
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
# Convert hyphens to dots (e.g., plain-models -> plain.models)
|
|
59
|
-
package = package.replace("-", ".")
|
|
60
|
-
|
|
61
|
-
# Automatically prefix if we need to
|
|
62
|
-
if not package.startswith("plain"):
|
|
63
|
-
package = f"plain.{package}"
|
|
64
|
-
|
|
65
|
-
try:
|
|
66
|
-
# Get the path for this specific package
|
|
67
|
-
spec = importlib.util.find_spec(package)
|
|
68
|
-
if not spec or not spec.origin:
|
|
69
|
-
raise click.UsageError(f"Package {package} not found")
|
|
70
|
-
|
|
71
|
-
package_path = Path(spec.origin).parent
|
|
72
|
-
paths = [package_path]
|
|
73
|
-
|
|
74
|
-
# Generate docs for this specific package
|
|
75
|
-
source_docs = LLMDocs(paths)
|
|
76
|
-
source_docs.load()
|
|
77
|
-
source_docs.print(relative_to=package_path.parent)
|
|
78
|
-
|
|
79
|
-
except Exception as e:
|
|
80
|
-
raise click.UsageError(f"Error loading documentation for {package}: {e}")
|
plain/cli/agent/md.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import importlib.util
|
|
2
|
-
import pkgutil
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
|
|
7
|
-
from ..output import iterate_markdown
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _get_packages_with_agents():
|
|
11
|
-
"""Get dict mapping package names to AGENTS.md paths."""
|
|
12
|
-
agents_files = {}
|
|
13
|
-
|
|
14
|
-
# Check for plain.* subpackages (including core plain)
|
|
15
|
-
try:
|
|
16
|
-
import plain
|
|
17
|
-
|
|
18
|
-
# Check core plain package (namespace package)
|
|
19
|
-
plain_spec = importlib.util.find_spec("plain")
|
|
20
|
-
if plain_spec and plain_spec.submodule_search_locations:
|
|
21
|
-
# For namespace packages, use the first search location
|
|
22
|
-
plain_path = Path(plain_spec.submodule_search_locations[0])
|
|
23
|
-
agents_path = plain_path / "AGENTS.md"
|
|
24
|
-
if agents_path.exists():
|
|
25
|
-
agents_files["plain"] = agents_path
|
|
26
|
-
|
|
27
|
-
# Check other plain.* subpackages
|
|
28
|
-
for importer, modname, ispkg in pkgutil.iter_modules(plain.__path__, "plain."):
|
|
29
|
-
if ispkg:
|
|
30
|
-
try:
|
|
31
|
-
spec = importlib.util.find_spec(modname)
|
|
32
|
-
if spec and spec.origin:
|
|
33
|
-
package_path = Path(spec.origin).parent
|
|
34
|
-
# Look for AGENTS.md at package root
|
|
35
|
-
agents_path = package_path / "AGENTS.md"
|
|
36
|
-
if agents_path.exists():
|
|
37
|
-
agents_files[modname] = agents_path
|
|
38
|
-
except Exception:
|
|
39
|
-
continue
|
|
40
|
-
except Exception:
|
|
41
|
-
pass
|
|
42
|
-
|
|
43
|
-
return agents_files
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@click.command("md")
|
|
47
|
-
@click.argument("package", default="", required=False)
|
|
48
|
-
@click.option(
|
|
49
|
-
"--all",
|
|
50
|
-
"show_all",
|
|
51
|
-
is_flag=True,
|
|
52
|
-
help="Show AGENTS.md for all packages that have them",
|
|
53
|
-
)
|
|
54
|
-
@click.option(
|
|
55
|
-
"--list",
|
|
56
|
-
"show_list",
|
|
57
|
-
is_flag=True,
|
|
58
|
-
help="List packages with AGENTS.md files",
|
|
59
|
-
)
|
|
60
|
-
def md(package, show_all, show_list):
|
|
61
|
-
"""Show AGENTS.md for a package."""
|
|
62
|
-
|
|
63
|
-
agents_files = _get_packages_with_agents()
|
|
64
|
-
|
|
65
|
-
if show_list:
|
|
66
|
-
for pkg in sorted(agents_files.keys()):
|
|
67
|
-
click.echo(f"- {pkg}")
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
if show_all:
|
|
72
|
-
for pkg in sorted(agents_files.keys()):
|
|
73
|
-
agents_path = agents_files[pkg]
|
|
74
|
-
for line in iterate_markdown(agents_path.read_text()):
|
|
75
|
-
click.echo(line, nl=False)
|
|
76
|
-
print()
|
|
77
|
-
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
if not package:
|
|
81
|
-
raise click.UsageError(
|
|
82
|
-
"Package name or --all required. Use --list to see available packages."
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
agents_path = agents_files[package]
|
|
86
|
-
for line in iterate_markdown(agents_path.read_text()):
|
|
87
|
-
click.echo(line, nl=False)
|
plain/cli/agent/prompt.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import shlex
|
|
3
|
-
import subprocess
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def is_agent_environment():
|
|
9
|
-
"""Check if we're running inside a coding agent."""
|
|
10
|
-
return bool(
|
|
11
|
-
os.environ.get("CLAUDECODE")
|
|
12
|
-
or os.environ.get("CODEX_SANDBOX")
|
|
13
|
-
or os.environ.get("CURSOR_ENVIRONMENT")
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def prompt_agent(
|
|
18
|
-
prompt: str, agent_command: str | None = None, print_only: bool = False
|
|
19
|
-
) -> bool:
|
|
20
|
-
if is_agent_environment():
|
|
21
|
-
click.echo(prompt)
|
|
22
|
-
return True
|
|
23
|
-
|
|
24
|
-
if print_only or not agent_command:
|
|
25
|
-
click.echo(prompt)
|
|
26
|
-
if not print_only:
|
|
27
|
-
click.secho(
|
|
28
|
-
"\nCopy the prompt above to a coding agent. To run an agent automatically, use --agent-command or set the PLAIN_AGENT_COMMAND environment variable.",
|
|
29
|
-
dim=True,
|
|
30
|
-
italic=True,
|
|
31
|
-
err=True,
|
|
32
|
-
)
|
|
33
|
-
return True
|
|
34
|
-
else:
|
|
35
|
-
cmd = shlex.split(agent_command)
|
|
36
|
-
cmd.append(prompt)
|
|
37
|
-
result = subprocess.run(cmd, check=False)
|
|
38
|
-
if result.returncode != 0:
|
|
39
|
-
click.secho(
|
|
40
|
-
f"Agent command failed with exit code {result.returncode}",
|
|
41
|
-
fg="red",
|
|
42
|
-
err=True,
|
|
43
|
-
)
|
|
44
|
-
return False
|
|
45
|
-
return True
|
plain/cli/agent/request.py
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
import click
|
|
4
|
-
|
|
5
|
-
from plain.runtime import settings
|
|
6
|
-
from plain.test import Client
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@click.command()
|
|
10
|
-
@click.argument("path")
|
|
11
|
-
@click.option(
|
|
12
|
-
"--method",
|
|
13
|
-
default="GET",
|
|
14
|
-
help="HTTP method (GET, POST, PUT, PATCH, DELETE, etc.)",
|
|
15
|
-
)
|
|
16
|
-
@click.option(
|
|
17
|
-
"--data",
|
|
18
|
-
help="Request data (JSON string for POST/PUT/PATCH)",
|
|
19
|
-
)
|
|
20
|
-
@click.option(
|
|
21
|
-
"--user",
|
|
22
|
-
"user_id",
|
|
23
|
-
help="User ID to authenticate as (skips normal authentication)",
|
|
24
|
-
)
|
|
25
|
-
@click.option(
|
|
26
|
-
"--follow/--no-follow",
|
|
27
|
-
default=True,
|
|
28
|
-
help="Follow redirects (default: True)",
|
|
29
|
-
)
|
|
30
|
-
@click.option(
|
|
31
|
-
"--content-type",
|
|
32
|
-
help="Content-Type header for request data",
|
|
33
|
-
)
|
|
34
|
-
@click.option(
|
|
35
|
-
"--header",
|
|
36
|
-
"headers",
|
|
37
|
-
multiple=True,
|
|
38
|
-
help="Additional headers (format: 'Name: Value')",
|
|
39
|
-
)
|
|
40
|
-
def request(path, method, data, user_id, follow, content_type, headers):
|
|
41
|
-
"""Make an HTTP request using the test client against the development database."""
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
# Only allow in DEBUG mode for security
|
|
45
|
-
if not settings.DEBUG:
|
|
46
|
-
click.secho("This command only works when DEBUG=True", fg="red", err=True)
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
# Temporarily add testserver to ALLOWED_HOSTS so the test client can make requests
|
|
50
|
-
original_allowed_hosts = settings.ALLOWED_HOSTS
|
|
51
|
-
settings.ALLOWED_HOSTS = ["*"]
|
|
52
|
-
|
|
53
|
-
try:
|
|
54
|
-
# Create test client
|
|
55
|
-
client = Client()
|
|
56
|
-
|
|
57
|
-
# If user_id provided, force login
|
|
58
|
-
if user_id:
|
|
59
|
-
try:
|
|
60
|
-
# Get the User model using plain.auth utility
|
|
61
|
-
from plain.auth import get_user_model
|
|
62
|
-
|
|
63
|
-
User = get_user_model()
|
|
64
|
-
|
|
65
|
-
# Get the user
|
|
66
|
-
try:
|
|
67
|
-
user = User.query.get(id=user_id)
|
|
68
|
-
client.force_login(user)
|
|
69
|
-
click.secho(
|
|
70
|
-
f"Authenticated as user {user_id}", fg="green", dim=True
|
|
71
|
-
)
|
|
72
|
-
except User.DoesNotExist:
|
|
73
|
-
click.secho(f"User {user_id} not found", fg="red", err=True)
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
except Exception as e:
|
|
77
|
-
click.secho(f"Authentication error: {e}", fg="red", err=True)
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
# Parse additional headers
|
|
81
|
-
header_dict = {}
|
|
82
|
-
for header in headers:
|
|
83
|
-
if ":" in header:
|
|
84
|
-
key, value = header.split(":", 1)
|
|
85
|
-
header_dict[key.strip()] = value.strip()
|
|
86
|
-
|
|
87
|
-
# Prepare request data
|
|
88
|
-
if data and content_type and "json" in content_type.lower():
|
|
89
|
-
try:
|
|
90
|
-
# Validate JSON
|
|
91
|
-
json.loads(data)
|
|
92
|
-
except json.JSONDecodeError as e:
|
|
93
|
-
click.secho(f"Invalid JSON data: {e}", fg="red", err=True)
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
# Make the request
|
|
97
|
-
method = method.upper()
|
|
98
|
-
kwargs = {
|
|
99
|
-
"path": path,
|
|
100
|
-
"follow": follow,
|
|
101
|
-
"headers": header_dict or None,
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if method in ("POST", "PUT", "PATCH") and data:
|
|
105
|
-
kwargs["data"] = data
|
|
106
|
-
if content_type:
|
|
107
|
-
kwargs["content_type"] = content_type
|
|
108
|
-
|
|
109
|
-
# Call the appropriate client method
|
|
110
|
-
if method == "GET":
|
|
111
|
-
response = client.get(**kwargs)
|
|
112
|
-
elif method == "POST":
|
|
113
|
-
response = client.post(**kwargs)
|
|
114
|
-
elif method == "PUT":
|
|
115
|
-
response = client.put(**kwargs)
|
|
116
|
-
elif method == "PATCH":
|
|
117
|
-
response = client.patch(**kwargs)
|
|
118
|
-
elif method == "DELETE":
|
|
119
|
-
response = client.delete(**kwargs)
|
|
120
|
-
elif method == "HEAD":
|
|
121
|
-
response = client.head(**kwargs)
|
|
122
|
-
elif method == "OPTIONS":
|
|
123
|
-
response = client.options(**kwargs)
|
|
124
|
-
elif method == "TRACE":
|
|
125
|
-
response = client.trace(**kwargs)
|
|
126
|
-
else:
|
|
127
|
-
click.secho(f"Unsupported HTTP method: {method}", fg="red", err=True)
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
# Display response information
|
|
131
|
-
click.secho(
|
|
132
|
-
f"HTTP {response.status_code}",
|
|
133
|
-
fg="green" if response.status_code < 400 else "red",
|
|
134
|
-
bold=True,
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
# Show additional response info first
|
|
138
|
-
if hasattr(response, "user"):
|
|
139
|
-
click.secho(f"Authenticated user: {response.user}", fg="blue", dim=True)
|
|
140
|
-
|
|
141
|
-
if hasattr(response, "resolver_match") and response.resolver_match:
|
|
142
|
-
match = response.resolver_match
|
|
143
|
-
url_name = match.namespaced_url_name or match.url_name or "unnamed"
|
|
144
|
-
click.secho(f"URL pattern matched: {url_name}", fg="blue", dim=True)
|
|
145
|
-
|
|
146
|
-
# Show headers
|
|
147
|
-
if response.headers:
|
|
148
|
-
click.secho("Response Headers:", fg="yellow", bold=True)
|
|
149
|
-
for key, value in response.headers.items():
|
|
150
|
-
click.echo(f" {key}: {value}")
|
|
151
|
-
click.echo()
|
|
152
|
-
|
|
153
|
-
# Show response content last
|
|
154
|
-
if response.content:
|
|
155
|
-
content_type = response.headers.get("Content-Type", "")
|
|
156
|
-
|
|
157
|
-
if "json" in content_type.lower():
|
|
158
|
-
try:
|
|
159
|
-
json_data = response.json()
|
|
160
|
-
click.secho("Response Body (JSON):", fg="yellow", bold=True)
|
|
161
|
-
click.echo(json.dumps(json_data, indent=2))
|
|
162
|
-
except Exception:
|
|
163
|
-
click.secho("Response Body:", fg="yellow", bold=True)
|
|
164
|
-
click.echo(response.content.decode("utf-8", errors="replace"))
|
|
165
|
-
elif "html" in content_type.lower():
|
|
166
|
-
click.secho("Response Body (HTML):", fg="yellow", bold=True)
|
|
167
|
-
content = response.content.decode("utf-8", errors="replace")
|
|
168
|
-
click.echo(content)
|
|
169
|
-
else:
|
|
170
|
-
click.secho("Response Body:", fg="yellow", bold=True)
|
|
171
|
-
content = response.content.decode("utf-8", errors="replace")
|
|
172
|
-
click.echo(content)
|
|
173
|
-
else:
|
|
174
|
-
click.secho("(No response body)", fg="yellow", dim=True)
|
|
175
|
-
|
|
176
|
-
finally:
|
|
177
|
-
# Restore original ALLOWED_HOSTS
|
|
178
|
-
settings.ALLOWED_HOSTS = original_allowed_hosts
|
|
179
|
-
|
|
180
|
-
except Exception as e:
|
|
181
|
-
click.secho(f"Request failed: {e}", fg="red", err=True)
|
plain/csrf/views.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
from plain.views import TemplateView
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class CsrfFailureView(TemplateView):
|
|
5
|
-
template_name = "403.html"
|
|
6
|
-
|
|
7
|
-
def get_response(self):
|
|
8
|
-
response = super().get_response()
|
|
9
|
-
response.status_code = 403
|
|
10
|
-
return response
|
|
11
|
-
|
|
12
|
-
def post(self):
|
|
13
|
-
return self.get()
|
|
14
|
-
|
|
15
|
-
def put(self):
|
|
16
|
-
return self.get()
|
|
17
|
-
|
|
18
|
-
def patch(self):
|
|
19
|
-
return self.get()
|
|
20
|
-
|
|
21
|
-
def delete(self):
|
|
22
|
-
return self.get()
|
|
23
|
-
|
|
24
|
-
def head(self):
|
|
25
|
-
return self.get()
|
|
26
|
-
|
|
27
|
-
def options(self):
|
|
28
|
-
return self.get()
|
|
29
|
-
|
|
30
|
-
def trace(self):
|
|
31
|
-
return self.get()
|
plain/logs/utils.py
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
request_logger = logging.getLogger("plain.request")
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def log_response(
|
|
7
|
-
message,
|
|
8
|
-
*args,
|
|
9
|
-
response=None,
|
|
10
|
-
request=None,
|
|
11
|
-
logger=request_logger,
|
|
12
|
-
level=None,
|
|
13
|
-
exception=None,
|
|
14
|
-
):
|
|
15
|
-
"""
|
|
16
|
-
Log errors based on Response status.
|
|
17
|
-
|
|
18
|
-
Log 5xx responses as errors and 4xx responses as warnings (unless a level
|
|
19
|
-
is given as a keyword argument). The Response status_code and the
|
|
20
|
-
request are passed to the logger's extra parameter.
|
|
21
|
-
"""
|
|
22
|
-
# Check if the response has already been logged. Multiple requests to log
|
|
23
|
-
# the same response can be received in some cases, e.g., when the
|
|
24
|
-
# response is the result of an exception and is logged when the exception
|
|
25
|
-
# is caught, to record the exception.
|
|
26
|
-
if getattr(response, "_has_been_logged", False):
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
if level is None:
|
|
30
|
-
if response.status_code >= 500:
|
|
31
|
-
level = "error"
|
|
32
|
-
elif response.status_code >= 400:
|
|
33
|
-
level = "warning"
|
|
34
|
-
else:
|
|
35
|
-
level = "info"
|
|
36
|
-
|
|
37
|
-
getattr(logger, level)(
|
|
38
|
-
message,
|
|
39
|
-
*args,
|
|
40
|
-
extra={
|
|
41
|
-
"status_code": response.status_code,
|
|
42
|
-
"request": request,
|
|
43
|
-
},
|
|
44
|
-
exc_info=exception,
|
|
45
|
-
)
|
|
46
|
-
response._has_been_logged = True
|
plain/preflight/messages.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# Levels
|
|
2
|
-
DEBUG = 10
|
|
3
|
-
INFO = 20
|
|
4
|
-
WARNING = 30
|
|
5
|
-
ERROR = 40
|
|
6
|
-
CRITICAL = 50
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class CheckMessage:
|
|
10
|
-
def __init__(self, level, msg, hint=None, obj=None, id=None):
|
|
11
|
-
if not isinstance(level, int):
|
|
12
|
-
raise TypeError("The first argument should be level.")
|
|
13
|
-
self.level = level
|
|
14
|
-
self.msg = msg
|
|
15
|
-
self.hint = hint
|
|
16
|
-
self.obj = obj
|
|
17
|
-
self.id = id
|
|
18
|
-
|
|
19
|
-
def __eq__(self, other):
|
|
20
|
-
return isinstance(other, self.__class__) and all(
|
|
21
|
-
getattr(self, attr) == getattr(other, attr)
|
|
22
|
-
for attr in ["level", "msg", "hint", "obj", "id"]
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
def __str__(self):
|
|
26
|
-
try:
|
|
27
|
-
from plain import models
|
|
28
|
-
|
|
29
|
-
ModelBase = models.base.ModelBase
|
|
30
|
-
using_db = True
|
|
31
|
-
except ImportError:
|
|
32
|
-
using_db = False
|
|
33
|
-
ModelBase = object
|
|
34
|
-
|
|
35
|
-
if self.obj is None:
|
|
36
|
-
obj = "?"
|
|
37
|
-
elif using_db and isinstance(self.obj, ModelBase):
|
|
38
|
-
# We need to hardcode ModelBase and Field cases because its __str__
|
|
39
|
-
# method doesn't return "applabel.modellabel" and cannot be changed.
|
|
40
|
-
obj = self.obj._meta.label
|
|
41
|
-
else:
|
|
42
|
-
obj = str(self.obj)
|
|
43
|
-
id = f"({self.id}) " if self.id else ""
|
|
44
|
-
hint = f"\n\tHINT: {self.hint}" if self.hint else ""
|
|
45
|
-
return f"{obj}: {id}{self.msg}{hint}"
|
|
46
|
-
|
|
47
|
-
def __repr__(self):
|
|
48
|
-
return f"<{self.__class__.__name__}: level={self.level!r}, msg={self.msg!r}, hint={self.hint!r}, obj={self.obj!r}, id={self.id!r}>"
|
|
49
|
-
|
|
50
|
-
def is_serious(self, level=ERROR):
|
|
51
|
-
return self.level >= level
|
|
52
|
-
|
|
53
|
-
def is_silenced(self):
|
|
54
|
-
from plain.runtime import settings
|
|
55
|
-
|
|
56
|
-
return self.id in settings.PREFLIGHT_SILENCED_CHECKS
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class Debug(CheckMessage):
|
|
60
|
-
def __init__(self, *args, **kwargs):
|
|
61
|
-
super().__init__(DEBUG, *args, **kwargs)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class Info(CheckMessage):
|
|
65
|
-
def __init__(self, *args, **kwargs):
|
|
66
|
-
super().__init__(INFO, *args, **kwargs)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class Warning(CheckMessage):
|
|
70
|
-
def __init__(self, *args, **kwargs):
|
|
71
|
-
super().__init__(WARNING, *args, **kwargs)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class Error(CheckMessage):
|
|
75
|
-
def __init__(self, *args, **kwargs):
|
|
76
|
-
super().__init__(ERROR, *args, **kwargs)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class Critical(CheckMessage):
|
|
80
|
-
def __init__(self, *args, **kwargs):
|
|
81
|
-
super().__init__(CRITICAL, *args, **kwargs)
|
plain/templates/AGENTS.md
DELETED