plain 0.61.0__py3-none-any.whl → 0.62.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/AGENTS.md +5 -1
- plain/CHANGELOG.md +12 -0
- plain/csrf/middleware.py +1 -1
- plain/internal/handlers/base.py +1 -1
- plain/internal/handlers/exception.py +1 -1
- plain/logs/README.md +104 -26
- plain/logs/__init__.py +1 -3
- plain/logs/configure.py +35 -43
- plain/logs/debug.py +36 -0
- plain/logs/formatters.py +70 -0
- plain/logs/loggers.py +182 -73
- plain/runtime/__init__.py +8 -4
- plain/runtime/global_settings.py +6 -2
- {plain-0.61.0.dist-info → plain-0.62.0.dist-info}/METADATA +2 -2
- {plain-0.61.0.dist-info → plain-0.62.0.dist-info}/RECORD +18 -16
- {plain-0.61.0.dist-info → plain-0.62.0.dist-info}/WHEEL +0 -0
- {plain-0.61.0.dist-info → plain-0.62.0.dist-info}/entry_points.txt +0 -0
- {plain-0.61.0.dist-info → plain-0.62.0.dist-info}/licenses/LICENSE +0 -0
plain/AGENTS.md
CHANGED
@@ -11,4 +11,8 @@ The `plain` CLI is the main entrypoint for the framework. If `plain` is not avai
|
|
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
13
|
- `plain agent request <path> --user <user_id>`: Make an authenticated request to the application and inspect the output.
|
14
|
-
- `plain --help`: List all available commands (including those from installed packages)
|
14
|
+
- `plain --help`: List all available commands (including those from installed packages).
|
15
|
+
|
16
|
+
## Code style
|
17
|
+
|
18
|
+
- Imports should be at the top of the file, unless there is a specific reason to import later (e.g. to avoid circular imports).
|
plain/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# plain changelog
|
2
2
|
|
3
|
+
## [0.62.0](https://github.com/dropseed/plain/releases/plain@0.62.0) (2025-09-09)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Complete rewrite of logging settings and AppLogger with improved formatters and debug capabilities ([ea7c953](https://github.com/dropseed/plain/commit/ea7c9537e3))
|
8
|
+
- Added `app_logger.debug_mode()` context manager to temporarily change log level ([f535459](https://github.com/dropseed/plain/commit/f53545f9fa))
|
9
|
+
- Minimum Python version updated to 3.13 ([d86e307](https://github.com/dropseed/plain/commit/d86e307efb))
|
10
|
+
|
11
|
+
### Upgrade instructions
|
12
|
+
|
13
|
+
- Make sure you are using Python 3.13 or higher
|
14
|
+
|
3
15
|
## [0.61.0](https://github.com/dropseed/plain/releases/plain@0.61.0) (2025-09-03)
|
4
16
|
|
5
17
|
### What's changed
|
plain/csrf/middleware.py
CHANGED
plain/internal/handlers/base.py
CHANGED
@@ -5,7 +5,7 @@ from opentelemetry import baggage, trace
|
|
5
5
|
from opentelemetry.semconv.attributes import http_attributes, url_attributes
|
6
6
|
|
7
7
|
from plain.exceptions import ImproperlyConfigured
|
8
|
-
from plain.logs import log_response
|
8
|
+
from plain.logs.utils import log_response
|
9
9
|
from plain.runtime import settings
|
10
10
|
from plain.urls import get_resolver
|
11
11
|
from plain.utils.module_loading import import_string
|
@@ -12,7 +12,7 @@ from plain.exceptions import (
|
|
12
12
|
)
|
13
13
|
from plain.http import Http404, ResponseServerError
|
14
14
|
from plain.http.multipartparser import MultiPartParserError
|
15
|
-
from plain.logs import log_response
|
15
|
+
from plain.logs.utils import log_response
|
16
16
|
from plain.runtime import settings
|
17
17
|
from plain.utils.module_loading import import_string
|
18
18
|
from plain.views.errors import ErrorView
|
plain/logs/README.md
CHANGED
@@ -4,7 +4,10 @@
|
|
4
4
|
|
5
5
|
- [Overview](#overview)
|
6
6
|
- [`app_logger`](#app_logger)
|
7
|
-
- [
|
7
|
+
- [Output formats](#output-formats)
|
8
|
+
- [Context management](#context-management)
|
9
|
+
- [Debug mode](#debug-mode)
|
10
|
+
- [Advanced usage](#advanced-usage)
|
8
11
|
- [Logging settings](#logging-settings)
|
9
12
|
|
10
13
|
## Overview
|
@@ -13,49 +16,124 @@ In Python, configuring logging can be surprisingly complex. For most use cases,
|
|
13
16
|
|
14
17
|
By default, both the `plain` and `app` loggers are set to the `INFO` level. You can quickly change this by using the `PLAIN_LOG_LEVEL` and `APP_LOG_LEVEL` environment variables.
|
15
18
|
|
19
|
+
The `app_logger` supports multiple output formats and provides a friendly kwargs-based API for structured logging.
|
20
|
+
|
16
21
|
## `app_logger`
|
17
22
|
|
18
|
-
The `app_logger` is
|
23
|
+
The `app_logger` is an enhanced logger that supports kwargs-style logging and multiple output formats.
|
19
24
|
|
20
25
|
```python
|
21
26
|
from plain.logs import app_logger
|
22
27
|
|
23
28
|
|
24
29
|
def example_function():
|
25
|
-
|
30
|
+
# Basic logging
|
31
|
+
app_logger.info("User logged in")
|
32
|
+
|
33
|
+
# With structured context data (explicit **context parameter)
|
34
|
+
app_logger.info("User action", user_id=123, action="login", success=True)
|
35
|
+
|
36
|
+
# All log levels support context parameters
|
37
|
+
app_logger.debug("Debug info", step="validation", count=5)
|
38
|
+
app_logger.warning("Rate limit warning", user_id=456, limit_exceeded=True)
|
39
|
+
app_logger.error("Database error", error_code=500, table="users")
|
40
|
+
|
41
|
+
# Standard logging parameters with context
|
42
|
+
try:
|
43
|
+
risky_operation()
|
44
|
+
except Exception:
|
45
|
+
app_logger.error(
|
46
|
+
"Operation failed",
|
47
|
+
exc_info=True, # Include exception traceback
|
48
|
+
stack_info=True, # Include stack trace
|
49
|
+
user_id=789,
|
50
|
+
operation="risky_operation"
|
51
|
+
)
|
26
52
|
```
|
27
53
|
|
28
|
-
##
|
54
|
+
## Output formats
|
55
|
+
|
56
|
+
The `app_logger` supports three output formats controlled by the `APP_LOG_FORMAT` environment variable:
|
29
57
|
|
30
|
-
|
58
|
+
### Key-Value format (default)
|
59
|
+
|
60
|
+
```bash
|
61
|
+
export APP_LOG_FORMAT=keyvalue # or leave unset for default
|
62
|
+
```
|
63
|
+
|
64
|
+
```
|
65
|
+
[INFO] User action user_id=123 action=login success=True
|
66
|
+
[ERROR] Database error error_code=500 table=users
|
67
|
+
```
|
68
|
+
|
69
|
+
### JSON format
|
70
|
+
|
71
|
+
```bash
|
72
|
+
export APP_LOG_FORMAT=json
|
73
|
+
```
|
74
|
+
|
75
|
+
```json
|
76
|
+
{"timestamp": "2024-01-01 12:00:00,123", "level": "INFO", "message": "User action", "user_id": 123, "action": "login", "success": true}
|
77
|
+
{"timestamp": "2024-01-01 12:00:01,456", "level": "ERROR", "message": "Database error", "error_code": 500, "table": "users"}
|
78
|
+
```
|
79
|
+
|
80
|
+
### Standard format
|
81
|
+
|
82
|
+
```bash
|
83
|
+
export APP_LOG_FORMAT=standard
|
84
|
+
```
|
85
|
+
|
86
|
+
```
|
87
|
+
[INFO] User action
|
88
|
+
[ERROR] Database error
|
89
|
+
```
|
90
|
+
|
91
|
+
Note: In standard format, the context kwargs are ignored and not displayed.
|
92
|
+
|
93
|
+
## Context management
|
94
|
+
|
95
|
+
The `app_logger` provides powerful context management for adding data to multiple log statements.
|
96
|
+
|
97
|
+
### Persistent context
|
98
|
+
|
99
|
+
Use the `context` dict to add data that persists across log calls:
|
31
100
|
|
32
101
|
```python
|
33
|
-
|
102
|
+
# Set persistent context
|
103
|
+
app_logger.context["user_id"] = 123
|
104
|
+
app_logger.context["request_id"] = "abc456"
|
34
105
|
|
106
|
+
app_logger.info("Started processing") # Includes user_id and request_id
|
107
|
+
app_logger.info("Validation complete") # Includes user_id and request_id
|
108
|
+
app_logger.info("Processing finished") # Includes user_id and request_id
|
35
109
|
|
36
|
-
|
37
|
-
|
110
|
+
# Clear context
|
111
|
+
app_logger.context.clear()
|
38
112
|
```
|
39
113
|
|
40
|
-
|
114
|
+
### Temporary context
|
41
115
|
|
42
|
-
|
116
|
+
Use `include_context()` for temporary context that only applies within a block:
|
43
117
|
|
44
118
|
```python
|
45
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
"
|
49
|
-
"
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
119
|
+
app_logger.context["user_id"] = 123 # Persistent
|
120
|
+
|
121
|
+
with app_logger.include_context(operation="payment", transaction_id="txn789"):
|
122
|
+
app_logger.info("Payment started") # Has user_id, operation, transaction_id
|
123
|
+
app_logger.info("Payment validated") # Has user_id, operation, transaction_id
|
124
|
+
|
125
|
+
app_logger.info("Payment complete") # Only has user_id
|
126
|
+
```
|
127
|
+
|
128
|
+
## Debug mode
|
129
|
+
|
130
|
+
The `force_debug()` context manager allows temporarily enabling DEBUG level logging:
|
131
|
+
|
132
|
+
```python
|
133
|
+
# Debug messages might not show at INFO level
|
134
|
+
app_logger.debug("This might not appear")
|
135
|
+
|
136
|
+
# Temporarily enable debug logging
|
137
|
+
with app_logger.force_debug():
|
138
|
+
app_logger.debug("This will definitely appear", extra_data="debug_info")
|
61
139
|
```
|
plain/logs/__init__.py
CHANGED
plain/logs/configure.py
CHANGED
@@ -1,44 +1,36 @@
|
|
1
1
|
import logging
|
2
|
-
|
3
|
-
from
|
4
|
-
|
5
|
-
|
6
|
-
def configure_logging(
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
},
|
38
|
-
},
|
39
|
-
}
|
40
|
-
logging.config.dictConfig(default_logging)
|
41
|
-
|
42
|
-
# Then customize it from settings
|
43
|
-
if logging_settings:
|
44
|
-
logging.config.dictConfig(logging_settings)
|
2
|
+
|
3
|
+
from .formatters import JSONFormatter, KeyValueFormatter
|
4
|
+
|
5
|
+
|
6
|
+
def configure_logging(*, plain_log_level, app_log_level, app_log_format):
|
7
|
+
# Create and configure the plain logger (uses standard Logger, not AppLogger)
|
8
|
+
plain_logger = logging.Logger("plain")
|
9
|
+
plain_logger.setLevel(plain_log_level)
|
10
|
+
plain_handler = logging.StreamHandler()
|
11
|
+
plain_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
12
|
+
plain_logger.addHandler(plain_handler)
|
13
|
+
plain_logger.propagate = False
|
14
|
+
logging.root.manager.loggerDict["plain"] = plain_logger
|
15
|
+
|
16
|
+
# Configure the existing app_logger
|
17
|
+
from .loggers import app_logger
|
18
|
+
|
19
|
+
app_logger.setLevel(app_log_level)
|
20
|
+
app_logger.propagate = False
|
21
|
+
|
22
|
+
app_handler = logging.StreamHandler()
|
23
|
+
match app_log_format:
|
24
|
+
case "json":
|
25
|
+
app_handler.setFormatter(JSONFormatter("%(json)s"))
|
26
|
+
case "keyvalue":
|
27
|
+
app_handler.setFormatter(
|
28
|
+
KeyValueFormatter("[%(levelname)s] %(message)s %(keyvalue)s")
|
29
|
+
)
|
30
|
+
case _:
|
31
|
+
app_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
32
|
+
|
33
|
+
app_logger.addHandler(app_handler)
|
34
|
+
|
35
|
+
# Register the app_logger in the logging system so getLogger("app") returns it
|
36
|
+
logging.root.manager.loggerDict["app"] = app_logger
|
plain/logs/debug.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
import logging
|
2
|
+
import threading
|
3
|
+
|
4
|
+
|
5
|
+
class DebugMode:
|
6
|
+
"""Context manager to temporarily set DEBUG level on a logger with reference counting."""
|
7
|
+
|
8
|
+
def __init__(self, logger):
|
9
|
+
self.logger = logger
|
10
|
+
self.original_level = None
|
11
|
+
self._ref_count = 0
|
12
|
+
self._lock = threading.Lock()
|
13
|
+
|
14
|
+
def __enter__(self):
|
15
|
+
"""Store original level and set to DEBUG."""
|
16
|
+
self.start()
|
17
|
+
return self
|
18
|
+
|
19
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
20
|
+
"""Restore original level."""
|
21
|
+
self.end()
|
22
|
+
|
23
|
+
def start(self):
|
24
|
+
"""Enable DEBUG logging level."""
|
25
|
+
with self._lock:
|
26
|
+
if self._ref_count == 0:
|
27
|
+
self.original_level = self.logger.level
|
28
|
+
self.logger.setLevel(logging.DEBUG)
|
29
|
+
self._ref_count += 1
|
30
|
+
|
31
|
+
def end(self):
|
32
|
+
"""Restore original logging level."""
|
33
|
+
with self._lock:
|
34
|
+
self._ref_count = max(0, self._ref_count - 1)
|
35
|
+
if self._ref_count == 0:
|
36
|
+
self.logger.setLevel(self.original_level)
|
plain/logs/formatters.py
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
|
4
|
+
|
5
|
+
class KeyValueFormatter(logging.Formatter):
|
6
|
+
"""Formatter that outputs key-value pairs from Plain's context system."""
|
7
|
+
|
8
|
+
def format(self, record):
|
9
|
+
# Build key-value pairs from context
|
10
|
+
kv_pairs = []
|
11
|
+
|
12
|
+
# Look for Plain's context data
|
13
|
+
if hasattr(record, "context") and isinstance(record.context, dict):
|
14
|
+
for key, value in record.context.items():
|
15
|
+
formatted_value = self._format_value(value)
|
16
|
+
kv_pairs.append(f"{key}={formatted_value}")
|
17
|
+
|
18
|
+
# Add the keyvalue attribute to the record for %(keyvalue)s substitution
|
19
|
+
record.keyvalue = " ".join(kv_pairs)
|
20
|
+
|
21
|
+
# Let the parent formatter handle the format string with %(keyvalue)s
|
22
|
+
return super().format(record)
|
23
|
+
|
24
|
+
@staticmethod
|
25
|
+
def _format_value(value):
|
26
|
+
"""Format a value for key-value output."""
|
27
|
+
if isinstance(value, str):
|
28
|
+
s = value
|
29
|
+
else:
|
30
|
+
s = str(value)
|
31
|
+
|
32
|
+
if '"' in s:
|
33
|
+
# Escape quotes and surround it
|
34
|
+
s = s.replace('"', '\\"')
|
35
|
+
s = f'"{s}"'
|
36
|
+
elif s == "":
|
37
|
+
# Quote empty strings instead of printing nothing
|
38
|
+
s = '""'
|
39
|
+
elif any(char in s for char in [" ", "/", "'", ":", "=", "."]):
|
40
|
+
# Surround these with quotes for parsers
|
41
|
+
s = f'"{s}"'
|
42
|
+
|
43
|
+
return s
|
44
|
+
|
45
|
+
|
46
|
+
class JSONFormatter(logging.Formatter):
|
47
|
+
"""Formatter that outputs JSON from Plain's context system, with optional format string."""
|
48
|
+
|
49
|
+
def format(self, record):
|
50
|
+
# Build the JSON object from Plain's context data
|
51
|
+
log_obj = {
|
52
|
+
"timestamp": self.formatTime(record),
|
53
|
+
"level": record.levelname,
|
54
|
+
"message": record.getMessage(),
|
55
|
+
"logger": record.name,
|
56
|
+
}
|
57
|
+
|
58
|
+
# Add Plain's context data to the main JSON object
|
59
|
+
if hasattr(record, "context") and isinstance(record.context, dict):
|
60
|
+
log_obj.update(record.context)
|
61
|
+
|
62
|
+
# Handle exceptions
|
63
|
+
if record.exc_info:
|
64
|
+
log_obj["exception"] = self.formatException(record.exc_info)
|
65
|
+
|
66
|
+
# Add the json attribute to the record for %(json)s substitution
|
67
|
+
record.json = json.dumps(log_obj, default=str, ensure_ascii=False)
|
68
|
+
|
69
|
+
# Let the parent formatter handle the format string with %(json)s
|
70
|
+
return super().format(record)
|
plain/logs/loggers.py
CHANGED
@@ -1,74 +1,183 @@
|
|
1
1
|
import logging
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
def info(
|
58
|
-
self
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
self.
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
2
|
+
from contextlib import contextmanager
|
3
|
+
|
4
|
+
from .debug import DebugMode
|
5
|
+
|
6
|
+
|
7
|
+
class AppLogger(logging.Logger):
|
8
|
+
"""Enhanced logger that supports kwargs-style logging and context management."""
|
9
|
+
|
10
|
+
def __init__(self, name):
|
11
|
+
super().__init__(name)
|
12
|
+
self.context = {} # Public, mutable context dict
|
13
|
+
self.debug_mode = DebugMode(self)
|
14
|
+
|
15
|
+
@contextmanager
|
16
|
+
def include_context(self, **kwargs):
|
17
|
+
"""Context manager for temporary context."""
|
18
|
+
# Store original context
|
19
|
+
original_context = self.context.copy()
|
20
|
+
|
21
|
+
# Add temporary context
|
22
|
+
self.context.update(kwargs)
|
23
|
+
|
24
|
+
try:
|
25
|
+
yield
|
26
|
+
finally:
|
27
|
+
# Restore original context
|
28
|
+
self.context = original_context
|
29
|
+
|
30
|
+
def force_debug(self):
|
31
|
+
"""Return context manager for temporarily enabling DEBUG level logging."""
|
32
|
+
return self.debug_mode
|
33
|
+
|
34
|
+
# Override logging methods with explicit parameters for IDE support
|
35
|
+
def debug(
|
36
|
+
self,
|
37
|
+
msg,
|
38
|
+
*args,
|
39
|
+
exc_info=None,
|
40
|
+
extra=None,
|
41
|
+
stack_info=False,
|
42
|
+
stacklevel=1,
|
43
|
+
**context,
|
44
|
+
):
|
45
|
+
if self.isEnabledFor(logging.DEBUG):
|
46
|
+
self._log(
|
47
|
+
logging.DEBUG,
|
48
|
+
msg,
|
49
|
+
args,
|
50
|
+
exc_info=exc_info,
|
51
|
+
extra=extra,
|
52
|
+
stack_info=stack_info,
|
53
|
+
stacklevel=stacklevel,
|
54
|
+
**context,
|
55
|
+
)
|
56
|
+
|
57
|
+
def info(
|
58
|
+
self,
|
59
|
+
msg,
|
60
|
+
*args,
|
61
|
+
exc_info=None,
|
62
|
+
extra=None,
|
63
|
+
stack_info=False,
|
64
|
+
stacklevel=1,
|
65
|
+
**context,
|
66
|
+
):
|
67
|
+
if self.isEnabledFor(logging.INFO):
|
68
|
+
self._log(
|
69
|
+
logging.INFO,
|
70
|
+
msg,
|
71
|
+
args,
|
72
|
+
exc_info=exc_info,
|
73
|
+
extra=extra,
|
74
|
+
stack_info=stack_info,
|
75
|
+
stacklevel=stacklevel,
|
76
|
+
**context,
|
77
|
+
)
|
78
|
+
|
79
|
+
def warning(
|
80
|
+
self,
|
81
|
+
msg,
|
82
|
+
*args,
|
83
|
+
exc_info=None,
|
84
|
+
extra=None,
|
85
|
+
stack_info=False,
|
86
|
+
stacklevel=1,
|
87
|
+
**context,
|
88
|
+
):
|
89
|
+
if self.isEnabledFor(logging.WARNING):
|
90
|
+
self._log(
|
91
|
+
logging.WARNING,
|
92
|
+
msg,
|
93
|
+
args,
|
94
|
+
exc_info=exc_info,
|
95
|
+
extra=extra,
|
96
|
+
stack_info=stack_info,
|
97
|
+
stacklevel=stacklevel,
|
98
|
+
**context,
|
99
|
+
)
|
100
|
+
|
101
|
+
def error(
|
102
|
+
self,
|
103
|
+
msg,
|
104
|
+
*args,
|
105
|
+
exc_info=None,
|
106
|
+
extra=None,
|
107
|
+
stack_info=False,
|
108
|
+
stacklevel=1,
|
109
|
+
**context,
|
110
|
+
):
|
111
|
+
if self.isEnabledFor(logging.ERROR):
|
112
|
+
self._log(
|
113
|
+
logging.ERROR,
|
114
|
+
msg,
|
115
|
+
args,
|
116
|
+
exc_info=exc_info,
|
117
|
+
extra=extra,
|
118
|
+
stack_info=stack_info,
|
119
|
+
stacklevel=stacklevel,
|
120
|
+
**context,
|
121
|
+
)
|
122
|
+
|
123
|
+
def critical(
|
124
|
+
self,
|
125
|
+
msg,
|
126
|
+
*args,
|
127
|
+
exc_info=None,
|
128
|
+
extra=None,
|
129
|
+
stack_info=False,
|
130
|
+
stacklevel=1,
|
131
|
+
**context,
|
132
|
+
):
|
133
|
+
if self.isEnabledFor(logging.CRITICAL):
|
134
|
+
self._log(
|
135
|
+
logging.CRITICAL,
|
136
|
+
msg,
|
137
|
+
args,
|
138
|
+
exc_info=exc_info,
|
139
|
+
extra=extra,
|
140
|
+
stack_info=stack_info,
|
141
|
+
stacklevel=stacklevel,
|
142
|
+
**context,
|
143
|
+
)
|
144
|
+
|
145
|
+
def _log(
|
146
|
+
self,
|
147
|
+
level,
|
148
|
+
msg,
|
149
|
+
args,
|
150
|
+
exc_info=None,
|
151
|
+
extra=None,
|
152
|
+
stack_info=False,
|
153
|
+
stacklevel=1,
|
154
|
+
**context,
|
155
|
+
):
|
156
|
+
"""Low-level logging routine which creates a LogRecord and then calls all handlers."""
|
157
|
+
# Check if extra already has a 'context' key
|
158
|
+
if extra and "context" in extra:
|
159
|
+
raise ValueError(
|
160
|
+
"The 'context' key in extra is reserved for Plain's context system"
|
161
|
+
)
|
162
|
+
|
163
|
+
# Build final extra with context
|
164
|
+
extra = extra.copy() if extra else {}
|
165
|
+
|
166
|
+
# Add our context (persistent + kwargs) to extra["context"]
|
167
|
+
if self.context or context:
|
168
|
+
extra["context"] = {**self.context, **context}
|
169
|
+
|
170
|
+
# Call the parent logger's _log method with explicit parameters
|
171
|
+
super()._log(
|
172
|
+
level=level,
|
173
|
+
msg=msg,
|
174
|
+
args=args,
|
175
|
+
exc_info=exc_info,
|
176
|
+
extra=extra or None,
|
177
|
+
stack_info=stack_info,
|
178
|
+
stacklevel=stacklevel,
|
179
|
+
)
|
180
|
+
|
181
|
+
|
182
|
+
# Create the default app logger instance
|
183
|
+
app_logger = AppLogger("app")
|
plain/runtime/__init__.py
CHANGED
@@ -3,6 +3,9 @@ import sys
|
|
3
3
|
from importlib.metadata import entry_points
|
4
4
|
from pathlib import Path
|
5
5
|
|
6
|
+
from plain.logs.configure import configure_logging
|
7
|
+
from plain.packages import packages_registry
|
8
|
+
|
6
9
|
from .user_settings import Settings
|
7
10
|
|
8
11
|
try:
|
@@ -48,9 +51,6 @@ def setup():
|
|
48
51
|
for entry_point in entry_points().select(group="plain.setup"):
|
49
52
|
entry_point.load()()
|
50
53
|
|
51
|
-
from plain.logs import configure_logging
|
52
|
-
from plain.packages import packages_registry
|
53
|
-
|
54
54
|
if not APP_PATH.exists():
|
55
55
|
raise AppPathNotFound(
|
56
56
|
"No app directory found. Are you sure you're in a Plain project?"
|
@@ -62,7 +62,11 @@ def setup():
|
|
62
62
|
if APP_PATH.parent.as_posix() not in sys.path:
|
63
63
|
sys.path.insert(0, APP_PATH.parent.as_posix())
|
64
64
|
|
65
|
-
configure_logging(
|
65
|
+
configure_logging(
|
66
|
+
plain_log_level=settings.PLAIN_LOG_LEVEL,
|
67
|
+
app_log_level=settings.APP_LOG_LEVEL,
|
68
|
+
app_log_format=settings.APP_LOG_FORMAT,
|
69
|
+
)
|
66
70
|
|
67
71
|
packages_registry.populate(settings.INSTALLED_PACKAGES)
|
68
72
|
|
plain/runtime/global_settings.py
CHANGED
@@ -3,6 +3,8 @@ Default Plain settings. Override these with settings in the module pointed to
|
|
3
3
|
by the PLAIN_SETTINGS_MODULE environment variable.
|
4
4
|
"""
|
5
5
|
|
6
|
+
from os import environ
|
7
|
+
|
6
8
|
from .utils import get_app_info_from_pyproject
|
7
9
|
|
8
10
|
# MARK: Core Settings
|
@@ -137,9 +139,11 @@ CSRF_TRUSTED_ORIGINS: list[str] = []
|
|
137
139
|
CSRF_EXEMPT_PATHS: list[str] = []
|
138
140
|
|
139
141
|
# MARK: Logging
|
142
|
+
# (Uses some custom env names in addition to PLAIN_ prefixed )
|
140
143
|
|
141
|
-
|
142
|
-
|
144
|
+
PLAIN_LOG_LEVEL: str = environ.get("PLAIN_LOG_LEVEL", "INFO")
|
145
|
+
APP_LOG_LEVEL: str = environ.get("APP_LOG_LEVEL", "INFO")
|
146
|
+
APP_LOG_FORMAT: str = environ.get("APP_LOG_FORMAT", "keyvalue")
|
143
147
|
|
144
148
|
# MARK: Assets
|
145
149
|
|
@@ -1,10 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: plain
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.62.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
|
7
|
-
Requires-Python: >=3.
|
7
|
+
Requires-Python: >=3.13
|
8
8
|
Requires-Dist: click>=8.0.0
|
9
9
|
Requires-Dist: jinja2>=3.1.2
|
10
10
|
Requires-Dist: opentelemetry-api>=1.34.1
|
@@ -1,5 +1,5 @@
|
|
1
|
-
plain/AGENTS.md,sha256=
|
2
|
-
plain/CHANGELOG.md,sha256=
|
1
|
+
plain/AGENTS.md,sha256=5XMGBpJgbCNIpp60DPXB7bpAtFk8FAzqiZke95T965o,1038
|
2
|
+
plain/CHANGELOG.md,sha256=qEE2843NosxXT0v1-2HE02EX3pesOtoaBDYBqSFCI8k,12429
|
3
3
|
plain/README.md,sha256=5BJyKhf0TDanWVbOQyZ3zsi5Lov9xk-LlJYCDWofM6Y,4078
|
4
4
|
plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
|
5
5
|
plain/debug.py,sha256=XdjnXcbPGsi0J2SpHGaLthhYU5AjhBlkHdemaP4sbYY,758
|
@@ -46,7 +46,7 @@ plain/cli/agent/md.py,sha256=7r1II8ckubBFOZNGPASWaPmJdgByWFPINLqIOzRetLQ,2581
|
|
46
46
|
plain/cli/agent/prompt.py,sha256=rugYyQHV7JDNqGrx3_PPShwwqYlnEVbxw8RsczOo8tg,1253
|
47
47
|
plain/cli/agent/request.py,sha256=JILrcxEMPagBXWrjNGMy3qatCYCXw-_uJMKkVHk_bho,6549
|
48
48
|
plain/csrf/README.md,sha256=ApWpB-qlEf0LkOKm9Yr-6f_lB9XJEvGFDo_fraw8ghI,2391
|
49
|
-
plain/csrf/middleware.py,sha256=
|
49
|
+
plain/csrf/middleware.py,sha256=n5_7v6qwFKgiAnKVyJa7RhwHoWepLkPudzIgZtdku5A,5119
|
50
50
|
plain/csrf/views.py,sha256=HwQqfI6KPelHP9gSXhjfZaTLQic71PKsoZ6DPhr1rKI,572
|
51
51
|
plain/forms/README.md,sha256=7MJQxNBoKkg0rW16qF6bGpUBxZrMrWjl2DZZk6gjzAU,2258
|
52
52
|
plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
|
@@ -70,17 +70,19 @@ plain/internal/files/uploadedfile.py,sha256=JRB7T3quQjg-1y3l1ASPxywtSQZhaeMc45uF
|
|
70
70
|
plain/internal/files/uploadhandler.py,sha256=63_QUwAwfq3bevw79i0S7zt2EB2UBoO7MaauvezaVMY,7198
|
71
71
|
plain/internal/files/utils.py,sha256=xN4HTJXDRdcoNyrL1dFd528MBwodRlHZM8DGTD_oBIg,2646
|
72
72
|
plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
73
|
-
plain/internal/handlers/base.py,sha256=
|
74
|
-
plain/internal/handlers/exception.py,sha256=
|
73
|
+
plain/internal/handlers/base.py,sha256=ur-nYmpvXjXhu03aPP1KV5GSNaLL_QZoT8x0v8l6_wg,6006
|
74
|
+
plain/internal/handlers/exception.py,sha256=TbPYtgZ7ITJahUKhQWkptHK28Lb4zh_nOviNctC2EYs,4815
|
75
75
|
plain/internal/handlers/wsgi.py,sha256=dgPT29t_F9llB-c5RYU3SHxGuZNaZ83xRjOfuOmtOl8,8209
|
76
76
|
plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
77
77
|
plain/internal/middleware/headers.py,sha256=ENIW1Gwat54hv-ejgp2R8QTZm-PlaI7k44WU01YQrNk,964
|
78
78
|
plain/internal/middleware/https.py,sha256=mS1YejfLB_5qlAoMfanh8Wn2O-RdSpOBdhvw2DRcTHs,1257
|
79
79
|
plain/internal/middleware/slash.py,sha256=JWcIfGbXEKH00I7STq1AMdHhFGmQHC8lkKENa6280ko,2846
|
80
|
-
plain/logs/README.md,sha256=
|
81
|
-
plain/logs/__init__.py,sha256=
|
82
|
-
plain/logs/configure.py,sha256=
|
83
|
-
plain/logs/
|
80
|
+
plain/logs/README.md,sha256=rzOHfngjizLgXL21g0svC1Cdya2s_gBA_E-IljtHpy8,4069
|
81
|
+
plain/logs/__init__.py,sha256=gFVMcNn5D6z0JrvUJgGsOeYj1NKNtEXhw0MvPDtkN6w,58
|
82
|
+
plain/logs/configure.py,sha256=G5kLP-92hOWE7vlWG3lhSbzOKXobavFbqjohevJF1Jg,1322
|
83
|
+
plain/logs/debug.py,sha256=QBXA_M498uGtqFnwHN08z6fItiGR4A732JyIWG2b39Q,1048
|
84
|
+
plain/logs/formatters.py,sha256=sHB4yo7806YN_V6cCzs1WOGMIZLq5q_eOndRqODI-T4,2380
|
85
|
+
plain/logs/loggers.py,sha256=kV2uZDxA5XU4GJF8dAOqny5LmbC8nPKZmakS-xX4x1Y,4625
|
84
86
|
plain/logs/utils.py,sha256=9UzdCCQXJinGDs71Ngw297mlWkhgZStSd67ya4NOW98,1257
|
85
87
|
plain/packages/README.md,sha256=iNqMtwFDVNf2TqKUzLKQW5Y4_GsssmdB4cVerzu27Ro,2674
|
86
88
|
plain/packages/__init__.py,sha256=OpQny0xLplPdPpozVUUkrW2gB-IIYyDT1b4zMzOcCC4,160
|
@@ -94,8 +96,8 @@ plain/preflight/registry.py,sha256=vcqzaE1MIneNL_ydKPy_1zrSThnzsrWARSClLCJ-4b8,2
|
|
94
96
|
plain/preflight/security.py,sha256=oxUZBp2M0bpBfUoLYepIxoex2Y90nyjlrL8XU8UTHYY,2438
|
95
97
|
plain/preflight/urls.py,sha256=cQ-WnFa_5oztpKdtwhuIGb7pXEml__bHsjs1SWO2YNI,1468
|
96
98
|
plain/runtime/README.md,sha256=sTqXXJkckwqkk9O06XMMSNRokAYjrZBnB50JD36BsYI,4873
|
97
|
-
plain/runtime/__init__.py,sha256=
|
98
|
-
plain/runtime/global_settings.py,sha256=
|
99
|
+
plain/runtime/__init__.py,sha256=byFYnHrpUCwkpkHtdNhxr9iUdLDCWJjy92HPj30Ilck,2478
|
100
|
+
plain/runtime/global_settings.py,sha256=cDhsZOh0FemxUQE41viBjoMOriXV9_JnbSu28Kon_uI,6014
|
99
101
|
plain/runtime/user_settings.py,sha256=OzMiEkE6ZQ50nxd1WIqirXPiNuMAQULklYHEzgzLWgA,11027
|
100
102
|
plain/runtime/utils.py,sha256=p5IuNTzc7Kq-9Ym7etYnt_xqHw5TioxfSkFeq1bKdgk,832
|
101
103
|
plain/signals/README.md,sha256=XefXqROlDhzw7Z5l_nx6Mhq6n9jjQ-ECGbH0vvhKWYg,272
|
@@ -158,8 +160,8 @@ plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
|
|
158
160
|
plain/views/objects.py,sha256=v3Vgvdoc1s0QW6JNWWrO5XXy9zF7vgwndgxX1eOSQoE,4999
|
159
161
|
plain/views/redirect.py,sha256=Xpb3cB7nZYvKgkNqcAxf9Jwm2SWcQ0u2xz4oO5M3vP8,1909
|
160
162
|
plain/views/templates.py,sha256=oAlebEyfES0rzBhfyEJzFmgLkpkbleA6Eip-8zDp-yk,1863
|
161
|
-
plain-0.
|
162
|
-
plain-0.
|
163
|
-
plain-0.
|
164
|
-
plain-0.
|
165
|
-
plain-0.
|
163
|
+
plain-0.62.0.dist-info/METADATA,sha256=vjydB1elvpE9PC6UhqU1F8B5eVxzWsoz2lrDJTnEYL0,4488
|
164
|
+
plain-0.62.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
165
|
+
plain-0.62.0.dist-info/entry_points.txt,sha256=nn4uKTRRZuEKOJv3810s3jtSMW0Gew7XDYiKIvBRR6M,93
|
166
|
+
plain-0.62.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
167
|
+
plain-0.62.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|