python-services — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited python-services (Agent Skill) and scored it 91/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 1 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} is the classic direct prompt-injection phrasing. Placed in a skill body that the agent reads as trusted instructions, it tries to make the agent abandon its prior rules and follow whatever comes next — a full system-prompt override.
ignore/disregard/forget … previous instructions sentence.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| Tool | Replaces | Purpose |
|---|---|---|
| uv | pip, virtualenv, pyenv, pipx | Package/dependency management |
| ruff | flake8, black, isort | Linting + formatting |
| ty | mypy, pyright | Type checking (Astral, faster) |
uv init --package myproject for distributable packages, uv init for appsuv add <pkg>, uv add --group dev <pkg>, never edit pyproject.toml deps manuallyuv run <cmd> instead of activating venvsuv.lock goes in version control[dependency-groups] (PEP 735) for dev/test/docs, not [project.optional-dependencies]ruff check --fix . && ruff format . for lint+format in one passEntry points in pyproject.toml:
[project.scripts]
my-tool = "my_package.cli:main"Click (recommended for complex CLIs):
import click
@click.group()
@click.version_option()
def cli(): ...
@cli.command()
@click.argument("name")
@click.option("--count", default=1, type=int)
def greet(name: str, count: int):
for _ in range(count):
click.echo(f"Hello, {name}!")
def main():
cli()argparse for simple CLIs — subparsers for subcommands, parser.add_argument("--output", "-o").
Use src/ layout. Include py.typed for type hints. importlib.resources.files() for package data access.
| Workload | Approach |
|---|---|
| Many concurrent I/O calls | asyncio (gather, create_task) |
| CPU-bound computation | multiprocessing.Pool or concurrent.futures.ProcessPoolExecutor |
| Mixed I/O + CPU | asyncio.to_thread() to offload blocking work |
| Simple scripts, few connections | Stay synchronous |
Key rule: Stay fully sync or fully async within a call path.
asyncio patterns:
asyncio.gather(*tasks) for concurrent I/O — use return_exceptions=True for partial failure toleranceasyncio.Semaphore(n) to limit concurrency (rate limiting external APIs)asyncio.wait_for(coro, timeout=N) for timeoutsasyncio.Queue for producer-consumerasyncio.Lock when coroutines share mutable stateasyncio.to_thread(sync_fn) for sync libs, aiohttp/httpx.AsyncClient for HTTPCancelledError — always re-raise after cleanupasync for) for streaming/paginationmultiprocessing for CPU-bound:
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor(max_workers=4) as pool:
results = list(pool.map(cpu_task, items))Project structure:
app/
├── api/v1/endpoints/ # Route handlers
├── core/ # config.py, security.py, database.py
├── models/ # SQLAlchemy models
├── schemas/ # Pydantic request/response
├── services/ # Business logic
├── repositories/ # Data access (generic CRUD base)
└── main.py # Lifespan, middleware, router includesLifespan for startup/shutdown: @asynccontextmanager async def lifespan(app):
Configuration — pydantic_settings.BaseSettings with model_config = {"env_file": ".env"}. Required fields = no default (fails fast at boot). env_nested_delimiter = "__" for grouped config. secrets_dir for Docker/K8s mounted secrets.
Dependency injection — Depends(get_db) for sessions, Depends(get_current_user) for auth. Override in tests: app.dependency_overrides[get_db] = mock_db.
Async DB — SQLAlchemy AsyncSession with asyncpg. Session-per-request via async with AsyncSessionLocal() as session: yield session.
Repository pattern — Generic BaseRepository[ModelType, CreateSchema, UpdateSchema] with get/get_multi/create/update/delete. Service layer holds business logic, routes stay thin.
/jobs/{id} for status@app.task(bind=True, max_retries=3, autoretry_for=(ConnectionError,)) — exponential backoff: raise self.retry(countdown=2**self.request.retries * 60)chain(a.s(), b.s()) for sequential, group(...) for parallel, chord(group, callback) for fan-out/fan-inRetries with tenacity:
from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type
@retry(
retry=retry_if_exception_type((ConnectionError, TimeoutError)),
stop=stop_after_attempt(5) | stop_after_delay(60),
wait=wait_exponential_jitter(initial=1, max=30),
before_sleep=log_retry_attempt,
)
def call_api(url: str) -> dict: ...@fail_safe(default=[]) decorator for non-critical paths — return cached/default on failure@traced @with_timeout(30) @retry(...) — separate infra from business logicJSONRenderer, TimeStamper, merge_contextvarsX-Correlation-ID header), bind to contextvars, propagate to downstream callsValueError, TypeError, KeyError, not bare Exceptionraise ServiceError("upload failed") from e — always chain to preserve debug trailBatchResult(succeeded={}, failed={}) — don't let one item abort the batchBaseModel with field_validator for complex input validation~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.