gpclog 0.2.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.
gpclog/__init__.py ADDED
@@ -0,0 +1,91 @@
1
+ """gpclog - General Purpose Category Logger.
2
+
3
+ 用于在复杂Python项目里管理分类日志。
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ from gpconfig import GPConfigFolder, GPConfigManager
9
+
10
+ from gpclog.config import GPCLoggerConfig
11
+ from gpclog.logger import GPCLogger
12
+ from gpclog.manager import GPCLoggerManager
13
+
14
+ __version__ = "0.2.0"
15
+
16
+ # Auto-register config and configurable classes with GPConfigManager
17
+ GPConfigManager.register_config_class(GPCLoggerConfig)
18
+ GPConfigManager.register_configurable_class(GPCLogger)
19
+
20
+ __all__ = [
21
+ "get_logger",
22
+ "set_config_folder",
23
+ "reset",
24
+ "GPCLogger",
25
+ "GPCLoggerConfig",
26
+ ]
27
+
28
+ # Singleton manager instance
29
+ _manager: GPCLoggerManager = GPCLoggerManager()
30
+
31
+
32
+ def get_logger(logger_name: str, sn: Optional[int] = None) -> GPCLogger:
33
+ """Get or create a logger by name.
34
+
35
+ This is the primary interface for obtaining loggers. Each unique
36
+ logger_name creates a separate logger with its own configuration
37
+ and output file.
38
+
39
+ Args:
40
+ logger_name: Unique name for the logger. This name is used
41
+ for configuration lookup and (without sn) for the log file name.
42
+ sn: Optional serial number for multiprocess isolation. When provided,
43
+ the logger is named "{logger_name}-{sn}" and writes to
44
+ "{logger_name}-{sn}.log", but still uses the configuration
45
+ associated with logger_name.
46
+
47
+ Returns:
48
+ GPCLogger instance for the given name.
49
+
50
+ Example:
51
+ >>> db_logger = gpclog.get_logger("database")
52
+ >>> db_logger.info("Connected to database")
53
+ >>>
54
+ >>> # In multiprocess scenario:
55
+ >>> worker_logger = gpclog.get_logger("worker", sn=process_id)
56
+ >>> worker_logger.info("Process started")
57
+ """
58
+ return _manager.get_logger(logger_name, sn)
59
+
60
+
61
+ def set_config_folder(cfg_folder: GPConfigFolder) -> None:
62
+ """Set the configuration folder for logger configurations.
63
+
64
+ After calling this function, logger configurations will be loaded
65
+ from the specified folder. If a logger's configuration is not found,
66
+ the default configuration is used.
67
+
68
+ Args:
69
+ cfg_folder: GPConfigFolder instance containing logger
70
+ configuration files.
71
+
72
+ Example:
73
+ >>> from gpconfig import GPConfigManager, GPConfigFolder
74
+ >>> manager = GPConfigManager("myapp")
75
+ >>> folder = GPConfigFolder(manager, "logs")
76
+ >>> gpclog.set_config_folder(folder)
77
+ """
78
+ _manager.set_config_folder(cfg_folder)
79
+
80
+
81
+ def reset() -> None:
82
+ """Reset the logger manager and clear all caches.
83
+
84
+ This function is primarily intended for testing purposes to ensure
85
+ a clean state between tests. It resets the internal state and clears
86
+ all cached loggers.
87
+ """
88
+ global _manager
89
+ GPCLoggerManager._instance = None
90
+ GPCLogger.clear_cache()
91
+ _manager = GPCLoggerManager()
gpclog/config.py ADDED
@@ -0,0 +1,55 @@
1
+ # src/gpclog/config.py
2
+ """GPCLoggerConfig - Configuration class for GPCLogger."""
3
+
4
+ from typing import ClassVar
5
+
6
+ from gpconfig import GPConfig
7
+
8
+
9
+ class GPCLoggerConfig(GPConfig):
10
+ """Configuration for GPCLogger.
11
+
12
+ Attributes:
13
+ cfg_class_name: Class identifier for gpconfig auto-detection.
14
+ configured_class_name: Name of the configurable class (always "GPCLogger").
15
+ level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
16
+ file_format: Format string for file output.
17
+ console_format: Format string for console output.
18
+ output_to_stdout: Whether to output to stdout.
19
+ output_to_stderr: Whether to output to stderr.
20
+ output_to_file: Whether to output to file.
21
+ log_path: Log output path (auto, env, home, or absolute path).
22
+ rotation_enabled: Whether to enable log rotation.
23
+ rotation_size: Rotation size threshold (e.g., "10 MB").
24
+ retention_enabled: Whether to enable log retention/deletion.
25
+ retention_days: Number of days to retain logs.
26
+ """
27
+
28
+ cfg_class_name: ClassVar[str] = "GPCLoggerConfig"
29
+
30
+ # Required fields
31
+ configured_class_name: str = "GPCLogger"
32
+ level: str = "INFO"
33
+
34
+ # Optional fields with defaults
35
+ file_format: str = (
36
+ "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[name]} | {message}"
37
+ )
38
+ console_format: str = (
39
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
40
+ "<level>{level: <8}</level> | "
41
+ "<cyan>{extra[name]}</cyan> | "
42
+ "<level>{message}</level>"
43
+ )
44
+
45
+ output_to_stdout: bool = True
46
+ output_to_stderr: bool = False
47
+ output_to_file: bool = True
48
+
49
+ log_path: str = "auto"
50
+
51
+ rotation_enabled: bool = False
52
+ rotation_size: str = "10 MB"
53
+
54
+ retention_enabled: bool = False
55
+ retention_days: int = 7
gpclog/logger.py ADDED
@@ -0,0 +1,149 @@
1
+ # src/gpclog/logger.py
2
+ """GPCLogger - Configurable logger class."""
3
+
4
+ import sys
5
+ from typing import Any, ClassVar
6
+
7
+ from gpconfig import GPConfigurable
8
+ from loguru import logger as loguru_logger
9
+
10
+ from gpclog.config import GPCLoggerConfig
11
+ from gpclog.utils import resolve_log_path
12
+
13
+
14
+ class GPCLogger(GPConfigurable):
15
+ """A configurable logger that wraps loguru.
16
+
17
+ This class provides a category-based logger with configuration support
18
+ through GPCLoggerConfig. Each logger instance writes to its own file
19
+ and can be configured independently.
20
+ """
21
+
22
+ # Class-level cache for bound loguru loggers to prevent duplicate handlers
23
+ _bound_loggers: ClassVar[dict[str, Any]] = {}
24
+
25
+ @classmethod
26
+ def clear_cache(cls) -> None:
27
+ """Clear the bound loggers cache.
28
+
29
+ This should be called when the singleton manager is reset to ensure
30
+ that cached loggers with stale handlers are not reused.
31
+ """
32
+ cls._bound_loggers.clear()
33
+
34
+ def __init__(self, config: GPCLoggerConfig) -> None:
35
+ """Initialize the logger with configuration.
36
+
37
+ Args:
38
+ config: GPCLoggerConfig instance containing logger settings.
39
+ """
40
+ super().__init__(config)
41
+ self._name: str = config.name or "default"
42
+ self._logger = self._create_logger(config)
43
+
44
+ @property
45
+ def name(self) -> str:
46
+ """Get the logger name."""
47
+ return self._name
48
+
49
+ def _create_logger(self, config: GPCLoggerConfig) -> Any:
50
+ """Create and configure a loguru logger.
51
+
52
+ Uses loguru's filter mechanism to allow multiple GPCLogger instances
53
+ to coexist and write to different files. Each logger binds its name
54
+ to the record's extra dict, and handlers use filters to route
55
+ messages to the appropriate sinks.
56
+
57
+ If a logger with the same name has already been bound, returns the
58
+ cached instance to avoid adding duplicate handlers.
59
+
60
+ Args:
61
+ config: Configuration for the logger.
62
+
63
+ Returns:
64
+ Configured loguru logger instance with bound name.
65
+ """
66
+ # Check if a logger with this name already exists in cache
67
+ if self._name in self._bound_loggers:
68
+ return self._bound_loggers[self._name]
69
+
70
+ # Create a logger filter function that captures the name by value
71
+ # to avoid closure issues with late binding
72
+ logger_name = self._name
73
+
74
+ def name_filter(record: dict) -> bool:
75
+ """Filter records by logger name."""
76
+ return record["extra"].get("name") == logger_name
77
+
78
+ # Create a new logger with bound context
79
+ new_logger = loguru_logger.bind(name=self._name)
80
+
81
+ # Add console handler if enabled
82
+ if config.output_to_stdout:
83
+ loguru_logger.add(
84
+ sink=sys.stdout,
85
+ format=config.console_format,
86
+ level=config.level,
87
+ colorize=True,
88
+ filter=name_filter,
89
+ )
90
+
91
+ # Add stderr handler if enabled
92
+ if config.output_to_stderr:
93
+ loguru_logger.add(
94
+ sink=sys.stderr,
95
+ format=config.console_format,
96
+ level=config.level,
97
+ colorize=True,
98
+ filter=name_filter,
99
+ )
100
+
101
+ # Add file handler if enabled
102
+ if config.output_to_file:
103
+ log_dir = resolve_log_path(config.log_path)
104
+ log_file = log_dir / f"{self._name}.log"
105
+
106
+ # Configure rotation
107
+ rotation = None
108
+ if config.rotation_enabled:
109
+ rotation = config.rotation_size
110
+
111
+ # Configure retention
112
+ retention = None
113
+ if config.retention_enabled:
114
+ retention = f"{config.retention_days} days"
115
+
116
+ loguru_logger.add(
117
+ sink=str(log_file),
118
+ format=config.file_format,
119
+ level=config.level,
120
+ rotation=rotation,
121
+ retention=retention,
122
+ encoding="utf-8",
123
+ filter=name_filter,
124
+ )
125
+
126
+ # Cache the bound logger to prevent duplicate handlers
127
+ self._bound_loggers[self._name] = new_logger
128
+
129
+ return new_logger
130
+
131
+ def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
132
+ """Log a debug message."""
133
+ self._logger.debug(message, *args, **kwargs)
134
+
135
+ def info(self, message: str, *args: Any, **kwargs: Any) -> None:
136
+ """Log an info message."""
137
+ self._logger.info(message, *args, **kwargs)
138
+
139
+ def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
140
+ """Log a warning message."""
141
+ self._logger.warning(message, *args, **kwargs)
142
+
143
+ def error(self, message: str, *args: Any, **kwargs: Any) -> None:
144
+ """Log an error message."""
145
+ self._logger.error(message, *args, **kwargs)
146
+
147
+ def critical(self, message: str, *args: Any, **kwargs: Any) -> None:
148
+ """Log a critical message."""
149
+ self._logger.critical(message, *args, **kwargs)
gpclog/manager.py ADDED
@@ -0,0 +1,201 @@
1
+ # src/gpclog/manager.py
2
+ """GPCLoggerManager - Singleton logger manager."""
3
+
4
+ from typing import ClassVar, Optional
5
+ from loguru import logger as loguru_logger
6
+
7
+ from gpconfig import GPConfigFolder
8
+
9
+ from gpclog.config import GPCLoggerConfig
10
+ from gpclog.logger import GPCLogger
11
+
12
+
13
+ class GPCLoggerManager:
14
+ """Singleton manager for GPCLogger instances.
15
+
16
+ This class manages all logger instances and provides a central
17
+ point for configuration management.
18
+ """
19
+
20
+ _instance: ClassVar[Optional["GPCLoggerManager"]] = None
21
+
22
+ def __new__(cls) -> "GPCLoggerManager":
23
+ """Create or return the singleton instance."""
24
+ if cls._instance is None:
25
+ cls._instance = super().__new__(cls)
26
+ cls._instance._initialize()
27
+ loguru_logger.remove()
28
+ # Clear the bound loggers cache since handlers were removed
29
+ GPCLogger.clear_cache()
30
+ return cls._instance
31
+
32
+ def _initialize(self) -> None:
33
+ """Initialize the manager instance."""
34
+ self._loggers: dict[tuple[str, Optional[int]], GPCLogger] = {}
35
+ self._config_folder: Optional[GPConfigFolder] = None
36
+ self._default_config: GPCLoggerConfig = self._create_default_config()
37
+
38
+ def _create_default_config(self) -> GPCLoggerConfig:
39
+ """Create the default configuration.
40
+
41
+ Returns:
42
+ GPCLoggerConfig with default values.
43
+ """
44
+ return GPCLoggerConfig(
45
+ name="default",
46
+ level="INFO",
47
+ )
48
+
49
+ @property
50
+ def default_config(self) -> GPCLoggerConfig:
51
+ """Get the default configuration."""
52
+ return self._default_config
53
+
54
+ @property
55
+ def config_folder(self) -> Optional[GPConfigFolder]:
56
+ """Get the config folder."""
57
+ return self._config_folder
58
+
59
+ def set_config_folder(self, cfg_folder: GPConfigFolder) -> None:
60
+ """Set the configuration folder for logger configs.
61
+
62
+ Args:
63
+ cfg_folder: GPConfigFolder instance containing logger configurations.
64
+ """
65
+ self._config_folder = cfg_folder
66
+
67
+ def get_logger(self, logger_name: str, sn: Optional[int] = None) -> GPCLogger:
68
+ """Get or create a logger by name.
69
+
70
+ If a logger with the given name and sn already exists, returns the cached
71
+ instance. Otherwise, creates a new logger with the appropriate
72
+ configuration.
73
+
74
+ Args:
75
+ logger_name: Unique name for the logger.
76
+ sn: Optional serial number. When provided, creates a logger named
77
+ "{logger_name}-{sn}" that uses the same configuration as
78
+ logger_name. Useful for multiprocess isolation.
79
+
80
+ Returns:
81
+ GPCLogger instance for the given name.
82
+ """
83
+ cache_key = (logger_name, sn)
84
+
85
+ # Check cache first
86
+ if cache_key in self._loggers:
87
+ return self._loggers[cache_key]
88
+
89
+ # Get or create configuration using original logger_name
90
+ config = self._get_config(logger_name)
91
+
92
+ # Override name to include sn if provided
93
+ if sn is not None:
94
+ config = GPCLoggerConfig(
95
+ name=f"{logger_name}-{sn}",
96
+ level=config.level,
97
+ file_format=config.file_format,
98
+ console_format=config.console_format,
99
+ output_to_stdout=config.output_to_stdout,
100
+ output_to_stderr=config.output_to_stderr,
101
+ output_to_file=config.output_to_file,
102
+ log_path=config.log_path,
103
+ rotation_enabled=config.rotation_enabled,
104
+ rotation_size=config.rotation_size,
105
+ retention_enabled=config.retention_enabled,
106
+ retention_days=config.retention_days,
107
+ )
108
+
109
+ # Create logger
110
+ logger = GPCLogger(config)
111
+
112
+ # Cache and return
113
+ self._loggers[cache_key] = logger
114
+ return logger
115
+
116
+ def _get_config(self, logger_name: str) -> GPCLoggerConfig:
117
+ """Get configuration for a logger.
118
+
119
+ Tries to load configuration from config folder if set.
120
+ Falls back to default configuration if not found.
121
+
122
+ Args:
123
+ logger_name: Name of the logger.
124
+
125
+ Returns:
126
+ GPCLoggerConfig for the logger.
127
+ """
128
+ # Try to get config from config folder
129
+ if self._config_folder is not None:
130
+ try:
131
+ config = self._config_folder.get_config(logger_name, GPCLoggerConfig)
132
+ # Merge with defaults for missing fields
133
+ return self._merge_with_defaults(config)
134
+ except Exception:
135
+ # Config not found, try default.yaml
136
+ try:
137
+ default_config = self._config_folder.get_config(
138
+ "default", GPCLoggerConfig
139
+ )
140
+ self._default_config = self._merge_with_defaults(default_config)
141
+ except Exception:
142
+ pass
143
+
144
+ # Use default config with the logger name
145
+ return GPCLoggerConfig(
146
+ name=logger_name,
147
+ level=self._default_config.level,
148
+ file_format=self._default_config.file_format,
149
+ console_format=self._default_config.console_format,
150
+ output_to_stdout=self._default_config.output_to_stdout,
151
+ output_to_stderr=self._default_config.output_to_stderr,
152
+ output_to_file=self._default_config.output_to_file,
153
+ log_path=self._default_config.log_path,
154
+ rotation_enabled=self._default_config.rotation_enabled,
155
+ rotation_size=self._default_config.rotation_size,
156
+ retention_enabled=self._default_config.retention_enabled,
157
+ retention_days=self._default_config.retention_days,
158
+ )
159
+
160
+ def _merge_with_defaults(self, config: GPCLoggerConfig) -> GPCLoggerConfig:
161
+ """Merge user config with defaults for missing fields.
162
+
163
+ Args:
164
+ config: User-provided configuration.
165
+
166
+ Returns:
167
+ GPCLoggerConfig with all fields filled.
168
+ """
169
+ defaults = self._default_config
170
+ return GPCLoggerConfig(
171
+ name=config.name,
172
+ level=config.level if config.level else defaults.level,
173
+ file_format=config.file_format
174
+ if config.file_format
175
+ else defaults.file_format,
176
+ console_format=config.console_format
177
+ if config.console_format
178
+ else defaults.console_format,
179
+ output_to_stdout=config.output_to_stdout
180
+ if config.output_to_stdout is not None
181
+ else defaults.output_to_stdout,
182
+ output_to_stderr=config.output_to_stderr
183
+ if config.output_to_stderr is not None
184
+ else defaults.output_to_stderr,
185
+ output_to_file=config.output_to_file
186
+ if config.output_to_file is not None
187
+ else defaults.output_to_file,
188
+ log_path=config.log_path if config.log_path else defaults.log_path,
189
+ rotation_enabled=config.rotation_enabled
190
+ if config.rotation_enabled is not None
191
+ else defaults.rotation_enabled,
192
+ rotation_size=config.rotation_size
193
+ if config.rotation_size
194
+ else defaults.rotation_size,
195
+ retention_enabled=config.retention_enabled
196
+ if config.retention_enabled is not None
197
+ else defaults.retention_enabled,
198
+ retention_days=config.retention_days
199
+ if config.retention_days is not None
200
+ else defaults.retention_days,
201
+ )
gpclog/utils.py ADDED
@@ -0,0 +1,60 @@
1
+ """Utility functions for gpclog."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def resolve_log_path(log_path: str) -> Path:
8
+ """Resolve the log output path.
9
+
10
+ Args:
11
+ log_path: Path specification. Can be:
12
+ - "auto": Auto-detect (GPCLOG_PATH env or home directory)
13
+ - "env": Use GPCLOG_PATH environment variable
14
+ - "home": Use user home directory
15
+ - Absolute path: Use specified path
16
+
17
+ Returns:
18
+ Resolved Path object pointing to the log output directory.
19
+
20
+ Raises:
21
+ ValueError: If path is invalid or cannot be resolved.
22
+ """
23
+ base_path: Path
24
+
25
+ if log_path == "auto":
26
+ # Try GPCLOG_PATH first, then fall back to home
27
+ env_path = os.environ.get("GPCLOG_PATH")
28
+ if env_path:
29
+ base_path = Path(env_path)
30
+ else:
31
+ base_path = Path.home()
32
+ # Print message to user
33
+ print(
34
+ f"gpclog: Using home directory for logs: {base_path / 'gpclog_output'}"
35
+ )
36
+ elif log_path == "env":
37
+ env_path = os.environ.get("GPCLOG_PATH")
38
+ if not env_path:
39
+ raise ValueError("GPCLOG_PATH environment variable is not set")
40
+ base_path = Path(env_path)
41
+ elif log_path == "home":
42
+ base_path = Path.home()
43
+ else:
44
+ # Treat as absolute path
45
+ base_path = Path(log_path)
46
+
47
+ # Validate path exists and is a directory
48
+ if not base_path.exists():
49
+ raise ValueError(f"Log path does not exist: {base_path}")
50
+ if not base_path.is_dir():
51
+ raise ValueError(f"Log path is not a directory: {base_path}")
52
+
53
+ # Check if already named gpclog_output
54
+ if base_path.name == "gpclog_output":
55
+ return base_path
56
+
57
+ # Create gpclog_output subfolder
58
+ final_path = base_path / "gpclog_output"
59
+ final_path.mkdir(exist_ok=True)
60
+ return final_path
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: gpclog
3
+ Version: 0.2.0
4
+ Summary: General Purpose Category Logger - A Python library for managing categorized logs in complex projects.
5
+ Project-URL: Homepage, https://github.com/linnetcodes/gpclog
6
+ Project-URL: Documentation, https://linnetcodes.github.io/gpclog
7
+ Project-URL: Repository, https://github.com/linnetcodes/gpclog
8
+ Project-URL: Issues, https://github.com/linnetcodes/gpclog/issues
9
+ Author-email: Linnet Codes <linnet.codes@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: categorized-logging,gpconfig,logging,loguru,python
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Topic :: System :: Logging
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: gpconfig>=0.3.0
29
+ Requires-Dist: loguru>=0.7.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # gpclog
37
+
38
+ [简体中文](README.zh-CN.md) | English
39
+
40
+ General Purpose Category Logger - A Python library for managing categorized logs in complex projects.
41
+
42
+ ## Features
43
+
44
+ - **Category-based Logging** - Each category has its own configuration and file
45
+ - **Built on loguru** - High-performance, feature-rich logging core
46
+ - **Built on gpconfig** - Type-safe configuration management
47
+ - **Smart Defaults** - Works out of the box with zero configuration
48
+ - **Auto Class Registration** - Automatically registers with GPConfigManager on import
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install gpclog
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ### Basic Usage
59
+
60
+ ```python
61
+ import gpclog
62
+
63
+ # Get a category logger (zero configuration)
64
+ logger = gpclog.get_logger("my_category")
65
+ logger.info("Hello, gpclog!")
66
+ # Output to: ~/gpclog_output/my_category.log
67
+ ```
68
+
69
+ ### Using Configuration Files
70
+
71
+ ```python
72
+ import gpclog
73
+ from gpconfig import GPConfigManager
74
+
75
+ # Initialize GPConfigManager (GPCLoggerConfig and GPCLogger are auto-registered)
76
+ manager = GPConfigManager("myapp")
77
+
78
+ # Method 1: Using gpclog public API
79
+ logs_folder = manager.get_config("logs")
80
+ gpclog.set_config_folder(logs_folder)
81
+ logger = gpclog.get_logger("database")
82
+
83
+ # Method 2: Using GPConfigManager directly
84
+ logger = manager.get_object("logs.database")
85
+ ```
86
+
87
+ Configuration schema, multiprocess usage, rotation/retention, and API details are documented in `docs/`.
88
+
89
+ ## Documentation
90
+
91
+ Build and validate the documentation locally:
92
+
93
+ ```bash
94
+ venv/bin/mkdocs build --clean --strict
95
+ ```
96
+
97
+ Preview the documentation locally:
98
+
99
+ ```bash
100
+ venv/bin/mkdocs serve
101
+ ```
102
+
103
+ ## API Reference
104
+
105
+ See:
106
+
107
+ - [Docs home](docs/index.md)
108
+ - [gpclog module](docs/gpclog.md)
109
+ - [GPCLogger](docs/logger.md)
110
+ - [GPCLoggerConfig](docs/config.md)
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,9 @@
1
+ gpclog/__init__.py,sha256=eHjAjHVU8S5SlI0iEHZMYzlhgH7vUMS8wVvuxvLKO5k,2949
2
+ gpclog/config.py,sha256=s3q3c_NlO-VTkRZ9oPbJ1LtG_1q3RUvgh4jQ0-F3miw,1812
3
+ gpclog/logger.py,sha256=D2L8DgiQCJhu-rL9M4qDRw7hrrrxfUrCWEkXNbm4M54,5045
4
+ gpclog/manager.py,sha256=auCzaYL6W_spAG-OFnN5W01n8rFnx1gVudQiVTyvdGI,7757
5
+ gpclog/utils.py,sha256=26CqwnSZS44go8-O8GlllyrSkW1ZC3tU54jobfpCLkY,1927
6
+ gpclog-0.2.0.dist-info/METADATA,sha256=KH1eIB2bcU4RLb_2QsqK270JHkRcRMoVG2uWIhQ03P4,3287
7
+ gpclog-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ gpclog-0.2.0.dist-info/licenses/LICENSE,sha256=iQ52cpW4_bx-09PoR6JiM2vuI_WTgsCBve1G9-ydmgg,1094
9
+ gpclog-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Linnet Codes <linnet.codes@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.