Skip to content

Latest commit

 

History

History
320 lines (238 loc) · 13.9 KB

File metadata and controls

320 lines (238 loc) · 13.9 KB

LLMs Guide - Snipepy

Context for large language models assisting with this project.

Architecture at a Glance

Snipepy is built on the Service Object / Facade pattern with supporting patterns and strict SOLID compliance. See ARCHITECTURE.md for the full design rationale.

Design Patterns

Pattern Where Purpose
Facade Snipepy class Single entry point; hides HTTP session, auth, and service wiring
Service Object *Service classes in src/snipepy/services/ One stateless class per SnipeIT resource group
Factory Method Snipepy.from_env(), Config.from_env() Centralise object creation from environment
Protocol (Structural Interface) HttpClientProtocol Decouple services from concrete HTTP implementation
Lazy Initialisation @cached_property on each service in Snipepy Services created only on first access

SOLID Compliance

Principle How it appears in Snipepy
S: Single Responsibility Config reads env vars; HttpClient makes HTTP calls; each *Service owns one resource group; models hold data only
O: Open/Closed New HTTP status codes are handled by adding one dict entry in _STATUS_EXCEPTIONS: no branching logic changes
L: Liskov Substitution Any HttpClientProtocol-satisfying object (e.g. a test mock) substitutes HttpClient without breaking service behaviour
I: Interface Segregation HttpClientProtocol exposes only the eight methods services actually call
D: Dependency Inversion Service classes import HttpClientProtocol (abstraction); Snipepy injects the concrete HttpClient at construction time

Key Rules

  • Snipepy is the facade: one entry point, hides auth and session setup.
  • Resource operations live on stateless service objects: client.assets.list(), client.users.get(id).
  • Service objects live in src/snipepy/services/: one file per resource group.
  • Models (Pydantic v2) are pure data containers: they have no methods that call the API.
  • HttpClient is the concrete HTTP layer; HttpClientProtocol is the typing.Protocol interface used by service classes (enables mock injection in tests).
  • Errors are raised as typed exceptions (SnipepyAuthError, SnipepyNotFoundError, …), never returned as None.

Push Safety

** NEVER push commits, branches, or tags to the remote repository without explicit human approval.**

This is a hard rule. You must never run git push, gh push, or any command that transfers local changes to the remote.

Your role is to:

  • Write and edit code.
  • Run local checks (tox).
  • Stage changes with git add and write commit messages with git commit.
  • Report what is ready for the human to push.

The human decides when and what reaches the remote. If a task requires pushing to remote, ask for permission first and wait for an explicit "yes".

What Snipepy Does

Snipepy wraps the SnipeIT REST API in Python. SnipeIT is an open-source IT asset management system. Snipepy lets scripts create, update, and query assets, users, licenses, and related resources without hand-crafting HTTP calls.

Repository Layout

src/snipepy/
  __init__.py          # public __all__: all exports live here
  snipepy.py             # Snipepy facade (cached_property services)
  _config.py           # Config frozen dataclass
  _http_client.py      # HttpClient, HttpClientProtocol, exception hierarchy
  _constants.py        # HTTP status code constants
  _whitelist.py        # Vulture allowlist
  py.typed             # PEP 561 marker
  models/              # Pydantic v2 response models (one file per resource)
  services/            # Service classes (one file per resource)

tests/
  unit/
    config/            # Config env parsing tests
    http_client/       # HttpClient unit tests (mocked session)
    models/            # Pydantic model validation tests
    services/          # Service class unit tests (mocked HttpClient)
    test_exceptions.py
    test_public_api.py
    test_snipepy.py
  smoke/               # Live Docker SnipeIT instance tests

API Response Shapes

SnipeIT returns two distinct envelope shapes depending on the operation:

List endpoints (GET /hardware, GET /users, …):

{
  "total": 244,
  "rows": [ { "id": 1, ... }, ... ]
}

Single-resource operations (create, update, checkout, …):

{
  "status": "success",
  "messages": "Asset created successfully",
  "payload": { "id": 300, "asset_tag": "T00000300", ... }
}

Validation errors (SnipeIT may return HTTP 200 with status: error):

{
  "status": "error",
  "messages": { "asset_tag": ["The asset tag has already been taken."] }
}

HttpClient._parse_payload() handles all three shapes: list responses return the full dict, success payloads return payload, and status: error raises SnipepyValidationError.

HttpClient Public Methods

Method HTTP verb Returns
get(path, params) GET dict or list
post(path, json) POST dict
put(path, json) PUT dict
patch(path, json) PATCH dict
delete(path) DELETE dict
upload(path, files) POST multipart dict
post_binary(path, json, accept) POST bytes
get_binary(path, params, accept) GET bytes

None-valued params are stripped before URL construction: service methods can pass optional keyword arguments directly without filtering.

Exception Hierarchy

SnipepyError
├── SnipepyAuthError        # 401
├── SnipepyForbiddenError   # 403
├── SnipepyNotFoundError    # 404
├── SnipepyConflictError    # 409
├── SnipepyValidationError  # HTTP 200 with status: error
└── SnipepyServerError      # 5xx

Service Method Conventions

Every service follows a consistent pattern:

service.list(limit, offset, search, **filters)  # GET, returns *List model
service.get(id)                                  # GET /<id>, returns model
service.create(**fields)                         # POST, returns model
service.update(id, **fields)                     # PUT /<id>, returns model
service.update_partial(id, **fields)             # PATCH /<id>, returns model
service.delete(id)                               # DELETE /<id>
service.restore(id)                              # POST /<id>/restore (soft-delete recovery)

Resource-specific extras (checkout, checkin, file upload, audit, etc.) are documented in ARCHITECTURE.md.

Where to Find Ground Truth

  • API definitions: docs/api-web-docs/: OpenAPI 3.1.0 specs for every endpoint.
  • Usage scenarios: docs/scenarios/: real workflows with curl examples and expected responses (written in Turkish).
  • Environment config: .env.example: all supported environment variables.
  • Public API: src/snipepy/__init__.py: the __all__ list is the authoritative public surface.
  • Why decisions were made: docs/adr/: Architecture Decision Records.
  • What was broken and how it was fixed: docs/fixes/: Fix Notes.

Fix Notes: Mandatory After Every Production Code Fix

Every fix that modifies source code under src/snipepy/ requires a Fix Note in docs/fixes/ before the task is considered complete. This is not optional. Do not report a task as finished until the Fix Note is written and added to the index in docs/fixes/README.md.

Why this rule exists

LLMs and agents tend to fix problems silently. The code changes but no record explains what was broken, why it was broken, or how the fix was verified. Future agents reading the codebase cannot distinguish "this is intentional" from "this was a workaround", and cannot learn from previous fixes. Fix Notes make every fix legible to any future reader.

What each section must contain

A Fix Note has four required sections. Each one has a specific job:

Section Job Common failure
Problem State the exact symptom: error message text, failing test names Writing "tests failed" instead of copying the error
Root Cause Explain the technical reason the code was wrong Restating the symptom ("the field was incorrect")
Fix Describe what changed and why it resolves the root cause Listing files without explaining the connection to the root cause
Verification Show named tests that now pass, before/after counts Writing "tests pass now" without specifics

The connection between Root Cause and Fix must be explicit. A reader who only reads the Fix section must be able to understand why that specific change was the correct one.

Naming

docs/fixes/NNNN-broken-behaviour-description.md

The title describes the broken behaviour, not the solution.

  • Correct: asset-assigned-to-accepts-integer
  • Wrong: fix-asset-model, update-validation, improve-error-handling

Severity

  • P1: caller receives wrong data silently, or data loss risk
  • P2: test failures, incorrect API responses, wrong field values
  • P3: edge case, minor, or internal tooling only

Full template and index

See docs/fixes/README.md for the template. See AGENTS.md for annotated examples showing acceptable vs unacceptable content.

Commit Messages

All commit messages follow Conventional Commits. Full rules are in CONTRIBUTING.md Section 8 and AGENTS.md.

Inspect before writing

Before writing any commit message, run:

git diff --staged   # staged changes
git diff            # unstaged changes
git status          # untracked/deleted files

The commit message must reflect what the diff contains, not what the user described. If the description contradicts the diff, follow the diff.

Structure

<type>[optional scope][optional !]: <description>

[optional body]

[optional footer(s)]

Type and SemVer mapping

Type When SemVer
feat New behaviour MINOR
fix Bug fix PATCH
! or BREAKING CHANGE footer Breaking API change MAJOR
docs, refactor, test, ci, chore, perf, style Other None

Decision tree

  1. Breaks existing public interface? Add ! and/or BREAKING CHANGE footer.
  2. New behaviour for callers? feat
  3. Fixes a bug? fix
  4. Documentation only? docs
  5. CI/CD only? ci
  6. Dependencies or tooling? chore
  7. Performance only? perf
  8. Restructure without behaviour change? refactor
  9. Tests only? test
  10. Formatting only? style

If a change fits multiple types, split it into multiple commits.

Breaking changes

feat(api)!: remove deprecated list_all() method

BREAKING CHANGE: list_all() is removed. Use list(limit=0) instead.

BREAKING CHANGE in footers must be uppercase.

Description rules

  • Imperative mood: "add", "fix", "remove", not "added" or "fixes".
  • Lowercase first letter.
  • No period at the end.
  • Max 72 characters for the entire first line.

Common mistakes

Incorrect Problem Correct
Added login feature No type prefix feat: add login feature
fix:token error Missing space after colon fix: resolve token expiry error
fix: bug Too vague fix: prevent null pointer in array parser
feat: add things and fix stuff Two concerns in one Split into two commits

Token and Credential Safety

  • Never generate, suggest, or reproduce API tokens in code, comments, or output.
  • Always load credentials from environment variables or .env files. Never hardcode them.
  • The .env file must never be committed to version control (it is already in .gitignore).

Writing Readable Code

Snipepy values readability over cleverness. When generating code:

  • Use self-documenting names: Function/method names should explain what and why. Avoid abbreviations (usr → user), single letters (x, y), generic names (data, info).
  • Comments in third-person: When comments are necessary, write them in English using third-person impersonal voice (e.g., "Checks if the response contains a valid token." or "The trailing slash is removed to prevent double slashes in URLs."). Never describe what the code does: the names should do that.
  • Type hints required: Every function must have parameter and return type hints.
  • Short, focused functions: If a function is hard to name, it is probably doing too much.

See AGENTS.md for the full guidelines.

SnipeIT-Specific Quirks to Keep in Mind

  • Trailing slash in SNIPEPY_URL: Config.from_env() strips a trailing slash from the URL. The client always appends /api/v1/<path>, so callers must not include the API prefix in the base URL.
  • status: error on HTTP 200: SnipeIT returns validation errors as {"status": "error", "messages": {...}} with HTTP 200. HttpClient._parse_payload() detects this and raises SnipepyValidationError. Any code that catches HTTP errors must also handle this case.
  • Paginated lists: List endpoints accept limit and offset. There is no cursor-based pagination: callers must iterate manually.
  • extra="allow" on all models: SnipeIT's API evolves; models accept unknown fields without raising errors. Do not rely on model_dump() returning only known fields.
  • Binary responses: Label generation and backup downloads return binary content (bytes), not JSON. Use post_binary() or get_binary() for these endpoints.
  • Checkout types: assets.checkout() accepts checkout_to_type of "user", "asset", or "location" with the corresponding assigned_user, assigned_asset, or assigned_location field.
  • Soft deletes: SnipeIT soft-deletes most resources. restore() is available on assets, users, companies, manufacturers, and categories.
  • Fieldset get_fields(): Fieldset.fields can be either a list or a paginated {"total": N, "rows": [...]} dict depending on the SnipeIT version. Use Fieldset.get_fields() to always get a flat list.