Organize documentation into topic folders (Claude, Python, Zscaler, Project template)

This commit is contained in:
2026-05-25 17:37:23 +02:00
parent 295a01aaa4
commit 46c4173440
22 changed files with 1203 additions and 0 deletions
+212
View File
@@ -0,0 +1,212 @@
# Python Development Guidelines
**Document Version:** v8
> **Note on Versioning:**
> - This document version is independent — reused across projects
> - **Project version** source of truth: `pyproject.toml`
> - Version propagates: `pyproject.toml` → `constants.py` → code
> - `CHANGELOG.md` uses project version from `pyproject.toml`
## Related Documents
- **README.md** — Project overview, tool descriptions, build instructions
- **AGENTS.md** — Rules for AI assistants
- **PROJECT.md** — Project goals and current state
- **CHANGELOG.md** — Version history
### Documentation Organization
All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root.
The root directory contains only the core documents: `DESIGN_DOCUMENT.md`, `AGENTS.md`, `PROJECT.md`, `CHANGELOG.md`.
---
## 1. Code Style
- **PEP8** with 150-character lines (Ruff)
- **4 spaces** indentation
- **snake_case** functions/variables, **PascalCase** classes, **SCREAMING_SNAKE_CASE** constants
- **Type hints** required on all functions
- **Import order**: stdlib → third-party → local
---
## 2. SOLID Principles
- **SRP** — One class = one responsibility
- **OCP** — Open for extension, closed for modification
- **LSP** — Subclasses substitutable for parents
- **ISP** — Small interfaces over large ones
- **DIP** — Depend on abstractions
---
## 3. Dependency Injection
Pass dependencies via constructor. Never instantiate dependencies inside a class.
---
## 4. Protocols Over Inheritance
Prefer `typing.Protocol` and composition over class inheritance.
---
## 5. Data Classes
Use `@dataclass` for internal data structures, `pydantic.BaseModel` for data that requires validation.
---
## 6. Logging and Console Output
### Logging — loguru
Use **loguru** for all internal logging. Never log secrets, passwords, tokens, or API keys.
#### Log sinks
| Sink | Level | Format |
|------|-------|--------|
| File `logs/{AppName}_{time}.log` | `DEBUG` | full (timestamp + level + message) |
| stdout | `INFO` | full |
File sink retains **max 10 log files** (`retention=10`). No rotation by size — each run creates a new file via `{time}` in the filename.
The `DEBUG` sink is only active when `constants.DEBUG` is `True` (controlled by `ENV_DEBUG=true` in `.env`).
Additional sinks (e.g. GUI log panels) may be added per project as needed.
#### Log levels
| Level | When to use |
|-------|-------------|
| `DEBUG` | Per-item detail: individual file reads/writes, cache hits, per-row operations |
| `INFO` | User-visible milestones: file loaded, process started/finished, file saved |
| `WARNING` | Recoverable issues: fallback used, unexpected but non-fatal state |
| `ERROR` | Failures the user must know about: missing file, failed save, missing config — operation cannot continue |
### Console output — print()
`print()` is **allowed** for direct user-facing console communication: input prompts, progress lines, and result summaries. This applies to console tools that interact with the user through stdin/stdout.
`print()` is **not** a substitute for loguru. It must not be used for debugging or internal event tracking.
---
## 7. Environment and Secrets
- Secrets in `.env` file; controlled by `ENV_DEBUG=true/false`
- Load via `python-dotenv` and `os.getenv()`
- Never commit `.env`
---
## 8. Error Handling
Define specific exception types. Use a fail-fast approach — surface errors early rather than silently continuing.
---
## 9. Testing
- **pytest** only — no `unittest`, no `TestCase` classes, no `self.assert*`
- Arrange-Act-Assert pattern
- Test naming: `test_<action>_<context>`
---
## 10. Tooling
| Tool | Purpose |
|------|---------|
| **Ruff** | Formatting and linting |
| **mypy** | Static type checking |
| **pytest** | Testing |
Run before every commit:
```bash
poetry run ruff check
poetry run mypy
```
---
## 11. Poetry
```bash
poetry install # Install all dependencies
poetry add <pkg> # Add runtime dependency
poetry add --group dev <pkg> # Add dev dependency
poetry remove <pkg> # Remove dependency
poetry run <cmd> # Run command in virtualenv
```
Never edit `pyproject.toml` directly to add or remove dependencies.
---
## 12. Project Structure
```
project/
├── entry_point.py # Entry point(s) — named by purpose or tool name
├── src/ # All application modules
├── tests/ # Tests
├── docs/ # Detailed documentation
├── .venv/ # Virtual environment (managed by Poetry)
└── pyproject.toml # Project config and dependencies
```
A project may have multiple entry points (e.g. `cli.py`, `gui.py`, or per-tool scripts).
---
## 13. Distribution and Deployment
When a project is distributed as a standalone executable (no Python required on target machine):
- Use **PyInstaller** to compile each entry point into a single `.exe`
- Each tool has its own `.spec` file in the project root
- All console tools must use `console=True` in the `.spec` — tools rely on `input()` and `print()` for user interaction
- Compiled executables are stored in `dist/` and **committed to the repository** — the repository serves as the distribution channel for internal teams
- `.gitignore` must **not** exclude `dist/` in projects that use this deployment model
Build command:
```bash
poetry run pyinstaller ToolName.spec
```
> This section applies only to projects that produce standalone executables. Skip for libraries or web services.
---
## 14. Versioning
- Follow **semantic versioning**: `MAJOR.MINOR.PATCH`
- Version is defined in `pyproject.toml` under `[project]`
- Always ask before bumping the version — never increment automatically
- Update `CHANGELOG.md` before bumping the version
---
## 15. Documentation and Task Management
- Keep `PROJECT.md` and `CHANGELOG.md` up to date when making changes
- Document architectural changes in this file or in `docs/`
### Task notation
Tasks are written as single-line comments directly in code, or in `PROJECT.md` for cross-cutting concerns:
```python
# TODO: one-liner description of a task to be done
# FIXME: one-liner description of a known bug to be fixed
```
No other task format is used — no checkboxes, no numbered lists in documentation.
If a `# TODO:` comment already exists at a specific location in code, do not repeat it in `PROJECT.md`.
+260
View File
@@ -0,0 +1,260 @@
# Python Library Development Guidelines
**Document Version:** v1
> **Note on Versioning:**
> - This document version is independent — reused across projects
> - **Project version** source of truth: `pyproject.toml` under `[project]`
> - `CHANGELOG.md` uses project version from `pyproject.toml`
## Related Documents
- **README.md** — Project overview, public API description, installation and usage examples
- **AGENTS.md** — Rules for AI assistants
- **PROJECT.md** — Project goals and current state
- **CHANGELOG.md** — Version history
### Documentation Organization
All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root.
The root directory contains only the core documents: `DESIGN_DOCUMENT_MODULE.md`, `AGENTS.md`, `PROJECT.md`, `CHANGELOG.md`.
---
## 1. Code Style
- **PEP8** with 150-character lines (Ruff)
- **4 spaces** indentation
- **snake_case** functions/variables, **PascalCase** classes, **SCREAMING_SNAKE_CASE** constants
- **Type hints** required on all functions
- **Import order**: stdlib → third-party → local
---
## 2. SOLID Principles
- **SRP** — One class = one responsibility
- **OCP** — Open for extension, closed for modification
- **LSP** — Subclasses substitutable for parents
- **ISP** — Small interfaces over large ones
- **DIP** — Depend on abstractions
---
## 3. Dependency Injection
Pass dependencies via constructor. Never instantiate dependencies inside a class.
---
## 4. Protocols Over Inheritance
Prefer `typing.Protocol` and composition over class inheritance.
---
## 5. Data Classes
Use `@dataclass` for internal data structures, `pydantic.BaseModel` for data that requires validation.
---
## 6. Logging
This is a library. Libraries must **never configure logging sinks** — that is the responsibility of the consuming application.
### Usage in library code
```python
from loguru import logger
def some_function() -> None:
logger.debug("Detail message")
logger.info("Milestone message")
```
Always use the module-level `logger` from loguru directly. Never call `logger.add()` or `logger.remove()` inside library code.
### Behavior for consumers
Loguru has a default sink to `stderr` enabled. Consumers who also use loguru get library logs automatically in their configured sinks. Consumers who want to suppress or filter library logs use the package name as the identifier:
```python
from loguru import logger
logger.disable("<package_name>") # suppress all
logger.add(sys.stderr, filter={"<package_name>": "WARNING"}) # WARNING and above only
logger.enable("<package_name>") # re-enable
```
This must be documented in `README.md`.
### Rules
- Never call `logger.add()` inside library code — no file sinks, no stdout sinks
- Never call `logger.remove()` inside library code
- Never log secrets, passwords, tokens, or API keys
- Never use `print()` inside library code — not for debugging, not for output
#### Log levels
| Level | When to use |
|-------|-------------|
| `DEBUG` | Per-item detail: individual operations, cache hits, internal state |
| `INFO` | Significant milestones visible to the consuming application |
| `WARNING` | Recoverable issues: fallback used, unexpected but non-fatal state |
| `ERROR` | Failures the caller must know about — operation cannot continue |
---
## 7. Environment and Secrets
- Libraries do not use `.env` files or `python-dotenv`
- Configuration is passed by the caller via arguments or constructor parameters
- Never read `os.getenv()` inside library code unless explicitly documented as a supported configuration mechanism
---
## 8. Error Handling
Define specific exception types in `src/<package>/exceptions.py`. Use a fail-fast approach — surface errors early rather than silently continuing. All public exceptions must be exported from the top-level `__init__.py`.
---
## 9. Testing
- **pytest** only — no `unittest`, no `TestCase` classes, no `self.assert*`
- Arrange-Act-Assert pattern
- Test naming: `test_<action>_<context>`
- Do not commit `poetry.lock` — this is a library; consumers pin their own dependencies
---
## 10. Tooling
| Tool | Purpose |
|------|---------|
| **Ruff** | Formatting and linting |
| **mypy** | Static type checking |
| **pytest** | Testing |
Run before every commit:
```bash
poetry run ruff check
poetry run mypy
poetry run pytest
```
---
## 11. Poetry
```bash
poetry install # Install all dependencies
poetry add <pkg> # Add runtime dependency
poetry add --group dev <pkg> # Add dev dependency
poetry remove <pkg> # Remove dependency
poetry run <cmd> # Run command in virtualenv
poetry build # Build sdist and wheel into dist/
poetry publish # Publish to PyPI
```
Never edit `pyproject.toml` directly to add or remove dependencies.
### pyproject.toml configuration for a library
```toml
[project]
name = "your-library"
version = "0.1.0"
description = "Short description"
requires-python = ">=3.x"
dependencies = []
[tool.poetry]
packages = [{include = "your_library", from = "src"}]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
```
Do **not** add `[tool.poetry.scripts]` — libraries have no entry points.
### poetry.lock
Do **not** commit `poetry.lock` for libraries. It is only committed for applications. Add `poetry.lock` to `.gitignore`.
---
## 12. Project Structure
```
project/
├── src/
│ └── your_library/
│ ├── __init__.py # Public API — export everything the caller needs
│ ├── exceptions.py # All public exception types
│ └── ... # Internal modules
├── tests/ # Tests
├── docs/ # Detailed documentation
├── .venv/ # Virtual environment (managed by Poetry)
└── pyproject.toml # Project config and dependencies
```
- No entry point scripts in the project root — this is a library, not an application
- `__init__.py` defines the public API; callers import from the top-level package only
---
## 13. Public API
- Everything intended for external use must be exported from `src/<package>/__init__.py`
- Use `__all__` to explicitly declare the public surface
- Internal modules are prefixed with `_` or kept unexported
- Public API must be stable across patch versions; breaking changes require a major version bump
---
## 14. Distribution
Build and publish with Poetry:
```bash
poetry build # Creates dist/*.tar.gz and dist/*.whl
poetry publish # Publishes to PyPI (requires credentials)
```
`dist/` is **not committed** to the repository — add it to `.gitignore`.
---
## 15. Versioning
- Follow **semantic versioning**: `MAJOR.MINOR.PATCH`
- Version is defined in `pyproject.toml` under `[project]`
- Always ask before bumping the version — never increment automatically
- Update `CHANGELOG.md` before bumping the version
- Breaking changes to the public API require a major version bump
---
## 16. Documentation and Task Management
- Keep `PROJECT.md` and `CHANGELOG.md` up to date when making changes
- `README.md` must contain installation instructions and usage examples for the public API
- Document architectural changes in this file or in `docs/`
### Task notation
Tasks are written as single-line comments directly in code, or in `PROJECT.md` for cross-cutting concerns:
```python
# TODO: one-liner description of a task to be done
# FIXME: one-liner description of a known bug to be fixed
```
No other task format is used — no checkboxes, no numbered lists in documentation.
If a `# TODO:` comment already exists at a specific location in code, do not repeat it in `PROJECT.md`.
+41
View File
@@ -0,0 +1,41 @@
# Template Generation Request
Create a `template/` folder in project root with reusable files for new Python projects.
## Required Structure
```
template/
├── .env # Environment variables (sample)
├── .gitignore # Git ignore rules
├── AGENTS.md # AI assistant rules
├── CHANGELOG.md # Changelog template
├── DESIGN_DOCUMENT.md # Development guidelines
├── PROJECT.md # Project documentation template
├── main.py # Entry point with loguru
├── pyproject.toml # Poetry config (ruff, mypy, pytest)
├── src/
│ ├── __init__.py
│ └── core/
│ ├── __init__.py
│ ├── _version.py # Version fallback for PyInstaller
│ └── constants.py # Version extraction from toml + DEBUG mode
└── tests/
├── __init__.py
└── test_constants.py # Basic test
```
## Key Features
- **Version extraction** from `pyproject.toml` with `_version.py` fallback for PyInstaller builds
- **DEBUG mode** via `ENV_DEBUG=true` in `.env` (adds " DEV" suffix to version)
- **loguru** for logging (never print)
- **Poetry** for dependency management
- **pytest** for testing (no unittest)
- **ruff + mypy** for linting and type checking
## Rules
- No `.example` suffixes - the folder itself is the separator
- Generic/reusable format
- Keep files simple and minimal
+66
View File
@@ -0,0 +1,66 @@
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
from src.constants import VERSION
load_dotenv()
print("=" * 50)
print("PREBUILD CONFIGURATION")
print("=" * 50)
# Check if running in virtual environment
project_root = Path(__file__).parent
expected_venv_path = project_root / ".venv"
current_executable = Path(sys.executable)
print(f"\nPython executable: {sys.executable}")
is_correct_venv = False
try:
current_executable.relative_to(expected_venv_path)
is_correct_venv = True
except ValueError:
is_correct_venv = False
if is_correct_venv:
print("✓ Correct environment selected for building")
else:
print("✗ Wrong environment selected")
print(f" Expected: {expected_venv_path}")
print(f" Current: {current_executable.parent.parent}")
print(f"✓ Version: {VERSION}")
env_debug = os.getenv("ENV_DEBUG", "false").lower() == "true"
console_mode = env_debug
default_spec = Path(__file__).parent.name + ".spec"
spec_filename = os.getenv("ENV_BUILD_SPEC", default_spec)
print(f"\n{'-' * 50}")
print("BUILD SETTINGS")
print(f"{'-' * 50}")
print(f"ENV_DEBUG: {env_debug}")
print(f"Console mode: {console_mode}")
print(f"Spec file: {spec_filename}")
spec_path = Path(__file__).parent / spec_filename
if spec_path.exists():
with open(spec_path, "r", encoding="utf-8") as f:
spec_content = f.read()
if f"console={not console_mode}" in spec_content:
new_spec_content = spec_content.replace(
f"console={not console_mode}",
f"console={console_mode}"
)
with open(spec_path, "w", encoding="utf-8") as f:
f.write(new_spec_content)
print(f"✓ Updated {spec_filename}: console={console_mode}")
else:
print(f"{spec_filename} already configured: console={console_mode}")
else:
print(f"{spec_filename} not found!")
print(f"{'-' * 50}\n")
View File
+7
View File
@@ -0,0 +1,7 @@
"""Auto-generated version - DO NOT EDIT MANUALLY!
This file is automatically generated from pyproject.toml.
Serves as fallback for cases when TOML is not available.
"""
__version__ = "0.1.0"
+81
View File
@@ -0,0 +1,81 @@
"""
Generic application constants template.
Usage in your project:
1. Copy this file to src/constants.py
2. Fill in APP_NAME and APP_FULL_NAME
3. Import VERSION, APP_TITLE, DEFAULT_DEBUG where needed
Version loading priority:
1. pyproject.toml [project] version (preferred)
2. src/_version.py __version__ (generated fallback for frozen builds)
3. "0.0.0" (last resort)
Debug mode:
Controlled exclusively via .env: ENV_DEBUG=true
Accepted true-values: true, 1, yes (case-insensitive)
"""
import os
import tomllib
from pathlib import Path
from dotenv import load_dotenv
from loguru import logger
load_dotenv()
# ---------------------------------------------------------------------------
# Version
# ---------------------------------------------------------------------------
_ROOT = Path(__file__).parent.parent
_PYPROJECT = _ROOT / "pyproject.toml"
_VERSION_FILE = Path(__file__).parent / "_version.py"
def _load_version() -> str:
# 1. pyproject.toml
try:
with open(_PYPROJECT, "rb") as f:
version = tomllib.load(f)["project"]["version"]
# Write fallback for frozen/PyInstaller builds
_VERSION_FILE.write_text(
f'"""Auto-generated — do not edit manually."""\n__version__ = "{version}"\n',
encoding="utf-8",
)
return version
except (FileNotFoundError, KeyError):
pass
# 2. _version.py
try:
from src._version import __version__ # type: ignore[import]
return __version__
except ImportError:
pass
# 3. last resort
return "0.0.0"
# ---------------------------------------------------------------------------
# Debug mode
# ---------------------------------------------------------------------------
def _load_debug() -> bool:
return os.getenv("ENV_DEBUG", "false").lower() in ("true", "1", "yes")
# ---------------------------------------------------------------------------
# Public constants ← fill in APP_NAME / APP_FULL_NAME for each project
# ---------------------------------------------------------------------------
APP_NAME: str = "MyApp"
APP_FULL_NAME: str = "My Application"
_VERSION_NUMBER: str = _load_version()
DEFAULT_DEBUG: bool = _load_debug()
VERSION: str = f"v{_VERSION_NUMBER}" + ("DEV" if DEFAULT_DEBUG else "")
APP_TITLE: str = f"{APP_FULL_NAME} {VERSION}"
View File
+141
View File
@@ -0,0 +1,141 @@
"""Tests for constants module."""
import re
from pathlib import Path
from unittest.mock import mock_open, patch
import pytest
from src.constants import (
APP_NAME,
APP_TITLE,
APP_VERSION,
ENV_DEBUG,
get_debug_mode,
get_version,
)
# ---------------------------------------------------------------------------
# get_version()
# ---------------------------------------------------------------------------
def test_get_version_returns_string() -> None:
"""get_version() should return a string."""
assert isinstance(get_version(), str)
def test_get_version_semver_format() -> None:
"""get_version() should return a semver-like string X.Y.Z."""
version = get_version()
assert re.match(r"^\d+\.\d+\.\d+", version), f"Not semver: {version!r}"
def test_get_version_fallback_when_toml_missing(tmp_path: Path) -> None:
"""get_version() returns '0.0.0-unknown' when pyproject.toml and _version.py are both missing."""
missing = tmp_path / "nonexistent.toml"
with patch("src.constants._PYPROJECT_PATH", missing):
result = get_version()
# Either fallback _version.py exists (from previous run) or returns unknown
assert isinstance(result, str)
assert len(result) > 0
def test_get_version_unknown_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""get_version() returns '0.0.0-unknown' when all sources are unavailable."""
missing = tmp_path / "nonexistent.toml"
monkeypatch.setattr("src.constants._PYPROJECT_PATH", missing)
# Patch _version import to also fail
with patch("src.constants.Path.write_text", side_effect=OSError):
with patch.dict("sys.modules", {"src._version": None}):
result = get_version()
assert isinstance(result, str)
# ---------------------------------------------------------------------------
# get_debug_mode()
# ---------------------------------------------------------------------------
def test_get_debug_mode_returns_bool() -> None:
"""get_debug_mode() should always return a bool."""
assert isinstance(get_debug_mode(), bool)
def test_get_debug_mode_true(monkeypatch: pytest.MonkeyPatch) -> None:
"""get_debug_mode() returns True when ENV_DEBUG=true."""
monkeypatch.setenv("ENV_DEBUG", "true")
assert get_debug_mode() is True
def test_get_debug_mode_true_variants(monkeypatch: pytest.MonkeyPatch) -> None:
"""get_debug_mode() accepts '1' and 'yes' as truthy values."""
for value in ("1", "yes", "YES", "True", "TRUE"):
monkeypatch.setenv("ENV_DEBUG", value)
assert get_debug_mode() is True, f"Expected True for ENV_DEBUG={value!r}"
def test_get_debug_mode_false(monkeypatch: pytest.MonkeyPatch) -> None:
"""get_debug_mode() returns False when ENV_DEBUG=false."""
monkeypatch.setenv("ENV_DEBUG", "false")
assert get_debug_mode() is False
def test_get_debug_mode_false_when_unset(monkeypatch: pytest.MonkeyPatch) -> None:
"""get_debug_mode() returns False when ENV_DEBUG is not set."""
monkeypatch.delenv("ENV_DEBUG", raising=False)
assert get_debug_mode() is False
# ---------------------------------------------------------------------------
# Module-level constants
# ---------------------------------------------------------------------------
def test_env_debug_is_bool() -> None:
"""ENV_DEBUG should be a bool."""
assert isinstance(ENV_DEBUG, bool)
def test_app_version_is_string() -> None:
"""APP_VERSION should be a string."""
assert isinstance(APP_VERSION, str)
def test_app_version_semver_format() -> None:
"""APP_VERSION should follow semver format X.Y.Z."""
assert re.match(r"^\d+\.\d+\.\d+", APP_VERSION), f"Not semver: {APP_VERSION!r}"
def test_app_name_value() -> None:
"""APP_NAME should be 'X4 SavEd'."""
assert APP_NAME == "X4 SavEd"
def test_app_title_contains_name_and_version() -> None:
"""APP_TITLE should contain APP_NAME and APP_VERSION."""
assert APP_NAME in APP_TITLE
assert APP_VERSION in APP_TITLE
def test_app_title_dev_suffix_when_debug(monkeypatch: pytest.MonkeyPatch) -> None:
"""APP_TITLE ends with '-DEV' when ENV_DEBUG is True."""
import importlib
import src.constants as consts
monkeypatch.setenv("ENV_DEBUG", "true")
monkeypatch.setattr(consts, "ENV_DEBUG", True)
title = f"{consts.APP_NAME} v{consts.APP_VERSION}" + ("-DEV" if True else "")
assert title.endswith("-DEV")
def test_app_title_no_dev_suffix_when_not_debug(monkeypatch: pytest.MonkeyPatch) -> None:
"""APP_TITLE does not end with '-DEV' when ENV_DEBUG is False."""
import src.constants as consts
monkeypatch.setattr(consts, "ENV_DEBUG", False)
title = f"{consts.APP_NAME} v{consts.APP_VERSION}" + ("-DEV" if False else "")
assert not title.endswith("-DEV")