Metadata-Version: 2.4
Name: dto-strict
Version: 0.1.0
Summary: AST-based linter for Python DTO discipline and facade-ban enforcement — framework-agnostic.
Project-URL: Homepage, https://github.com/jekhator/dto-strict
Project-URL: Repository, https://github.com/jekhator/dto-strict.git
Project-URL: Issues, https://github.com/jekhator/dto-strict/issues
Author: James Ekhator
License: Apache-2.0
License-File: LICENSE
Keywords: ast,code-quality,dataclass,dto,linter,static-analysis
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# dto-strict

AST-based linter for Python DTO discipline and facade-ban enforcement — pluggable, framework-agnostic.

## Why dto-strict?

Data Transfer Objects (DTOs) provide a critical boundary between services and prevent the fragmentation of business-logic definitions across codebases. However, when function signatures leak `Dict[str, Any]` or when services build dict literals inline instead of using structured DTOs, code becomes:

- **Loosely typed**: Shape mismatches only surface at runtime.
- **Duplicated**: The same business object gets redefined wherever it's used.
- **Hard to evolve**: Changing a field requires updating dicts in 10+ places.

Facade functions (module-level helpers that wrap framework machinery) similarly tend to proliferate and obscure intent when unmarked. The "facade—celery schedule" pattern makes intent explicit.

**dto-strict** enforces DTO and facade discipline via static AST analysis, with 5 focused rules:

1. **R001 (HIGH)**: Detect `Dict[str, Any]` in service-layer function signatures.
2. **R002 (MEDIUM)**: Flag inline dict literals with 3+ string keys.
3. **R003 (MEDIUM)**: Require `@dataclass(frozen=True, slots=True, repr=False)` trio in DTOs.
4. **R004 (HIGH)**: Demand exception tags on module-level functions (e.g., `# facade — celery schedule`).
5. **R005 (LOW)**: Encourage validators to use `DTO.from_dict()` pattern.

All rules are configurable; violations can be disabled, severity overridden, or paths scoped.

## Install

```bash
pip install dto-strict
```

## Quick Start

### Basic CLI Usage

```bash
# Lint a single file
dto-strict apps/compliance/services.py

# Lint a directory
dto-strict apps/

# Output as GitHub Actions annotations
dto-strict apps/ --format github

# Output as JSON
dto-strict apps/ --format json
```

### Configuration (pyproject.toml)

```toml
[tool.dto-strict]
service_paths = [
    "apps/*/services/*.py",
    "**/services/*.py",
]
dto_paths = [
    "**/dtos.py",
    "**/dtos/*.py",
]
exception_tags = [
    "facade — celery schedule",
    "FRAMEWORK",
]
disabled_rules = ["R005"]  # Disable low-priority rules if desired
severity_overrides = { "R002" = "low" }  # Downgrade specific rules
```

### GitHub Actions

Create `.github/workflows/dto-strict.yml`:

```yaml
name: dto-strict
on:
  pull_request:
    paths: ['apps/**.py']

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install dto-strict
      - run: dto-strict apps/ --format github
```

### Pre-commit Hook

Add to `.pre-commit-config.yaml`:

```yaml
- repo: local
  hooks:
    - id: dto-strict
      name: dto-strict
      entry: dto-strict
      language: python
      types: [python]
      additional_dependencies: ['dto-strict']
      stages: [commit]
```

## Rules

### R001: Dict[str, Any] in Service Signatures (HIGH)

Service-layer functions should not accept or return `Dict[str, Any]`. Use a DTO instead.

**Fail:**
```python
def process_user(config: Dict[str, Any]) -> Dict[str, Any]:
    return {"status": "ok"}
```

**Pass:**
```python
@dataclass(frozen=True, slots=True, repr=False)
class UserConfigDTO:
    timeout: int
    retries: int

def process_user(config: UserConfigDTO) -> dict:
    return {"status": "ok"}
```

**Rationale:** Typed parameters enable IDE completion and catch shape mismatches early.

---

### R002: Inline Dict Literals (MEDIUM)

Service files with inline dict literals containing 3+ string keys should define a DTO instead.

**Fail:**
```python
def build_response(user_id: int) -> dict:
    return {
        "user_id": user_id,
        "status": "active",
        "timestamp": "2025-01-01",
    }
```

**Pass:**
```python
@dataclass(frozen=True, slots=True, repr=False)
class ResponseDTO:
    user_id: int
    status: str
    timestamp: str

def build_response(user_id: int) -> ResponseDTO:
    return ResponseDTO(user_id, "active", "2025-01-01")
```

**Rationale:** Shared shapes should live in DTOs. Inline dicts make duplication invisible.

---

### R003: Dataclass Decorator Trio (MEDIUM)

All `@dataclass` definitions in DTO files must include `frozen=True`, `slots=True`, and `repr=False`.

**Fail:**
```python
@dataclass
class UserDTO:
    user_id: int

@dataclass(frozen=True)
class ConfigDTO:
    timeout: int
```

**Pass:**
```python
@dataclass(frozen=True, slots=True, repr=False)
class UserDTO:
    user_id: int

@dataclass(frozen=True, slots=True, repr=False)
class ConfigDTO:
    timeout: int
```

**Rationale:**
- `frozen=True`: Immutability enforces single-source-of-truth.
- `slots=True`: Memory efficiency and prevents attribute typos.
- `repr=False`: Prevents accidental logging of sensitive fields in tracebacks.

---

### R004: Module-Level Functions (HIGH)

Bare module-level functions (facades, framework hooks) must carry an exception tag in a comment or docstring.

**Fail:**
```python
def process_user(user_id: int):
    pass

def send_notification(message: str):
    pass
```

**Pass:**
```python
def process_user(user_id: int):  # facade — celery schedule
    pass

def send_notification(message: str):  # FRAMEWORK
    """Send via SNS."""
    pass

class UserService:
    def process(self, user_id: int):
        # Class methods don't need tags
        pass
```

**Exception Tags:** Configurable via `pyproject.toml` `exception_tags` list.

**Rationale:** Facades blur intent. Tags make intent explicit and signal "this is framework-specific, not business logic."

---

### R005: Validator Pattern (LOW)

`validate_*()` functions should use `DTO.from_dict()` or raise `ValidationError` to enforce payload shape.

**Fail:**
```python
def validate_user_payload(payload: dict) -> bool:
    return "user_id" in payload and "email" in payload
```

**Pass:**
```python
def validate_user_payload(payload: dict) -> UserDTO:
    try:
        user = UserDTO(
            user_id=payload["user_id"],
            email=payload["email"],
        )
        return user
    except (KeyError, TypeError) as e:
        raise ValidationError(f"Invalid shape: {e}")
```

**Rationale:** Validators should enforce structure, not just presence.

---

## Output Formats

### Text (default)

```
app.py:10: R001 Dict[str, Any] in signature: process_user
service.py:20: R002 Inline dict literal with 4 keys
```

### GitHub Actions

```
::error file=app.py,line=10,col=5::R001 Dict[str, Any] in signature: process_user
::warning file=service.py,line=20,col=0::R002 Inline dict literal with 4 keys
```

### JSON

```json
[
  {
    "rule_id": "R001",
    "severity": "HIGH",
    "file": "app.py",
    "line": 10,
    "col": 5,
    "message": "Dict[str, Any] in signature: process_user"
  }
]
```

## Exit Codes

| Code | Meaning |
|------|---------|
| 0    | No violations |
| 1    | HIGH severity violations present |
| 2    | MEDIUM severity violations only |
| 3    | LOW severity violations only |

## Configuration Reference

```toml
[tool.dto-strict]

# Paths to check for service-layer violations (R001, R002, R004)
# Default: ["apps/*/services/*.py", "**/services/*.py"]
service_paths = [
    "apps/*/services/*.py",
    "**/services/*.py",
]

# Paths to check for DTO definitions (R003)
# Default: ["**/dtos.py", "**/dtos/*.py"]
dto_paths = [
    "**/dtos.py",
    "**/dtos/*.py",
]

# Allowed exception tags for R004 (module-level facades)
# Default: ["facade — celery schedule", "FRAMEWORK"]
exception_tags = [
    "facade — celery schedule",
    "FRAMEWORK",
    "CUSTOM_TAG",
]

# Disable specific rules entirely
# Default: []
disabled_rules = ["R005"]

# Override severity for specific rules
# Valid values: "HIGH", "MEDIUM", "LOW"
# Default: {}
severity_overrides = {
    "R002" = "low",
}
```

## Design Philosophy

**Pluggable, not opinionated.** Every rule is:

- **Configurable**: Path patterns, exception tags, severity levels.
- **Disable-able**: Set `disabled_rules = ["R001"]` to skip it entirely.
- **Framework-agnostic**: No Django/FastAPI/Flask assumptions; adapters for each framework are opt-in extras.

**Defaults bundled, not imposed.** Out-of-the-box rules target Django + DRF + Celery patterns, but you can customize for your stack.

## Development

```bash
git clone https://github.com/jekhator/dto-strict.git
cd dto-strict
python3 -m venv .venv && source .venv/bin/activate
pip install -e .[dev]

# Run tests
pytest tests/ -v

# Run linter on itself
dto-strict src/ --format github
```

## License

Apache License 2.0. See LICENSE.

## Contributing

Issues and PRs welcome. Please include fixtures (good + bad examples) for new rules.

## See Also

- [pii-aware-mixin](https://github.com/jekhator/pii-aware-mixin) — Auto-hide PII in dataclass repr/logging.
- [logging-mixin](https://github.com/jekhator/logging-mixin) — Class-bound structured logging with correlation IDs.
