Reusable API response and exception handling utilities with FastAPI integration.
Project description
aniket_tools
aniket_tools is a reusable Python library for three things:
- building consistent API responses
- translating exceptions into user-friendly messages
- logging raw technical failures for developers
This README only covers library usage and code behavior.
Install
pip install aniket_tools
For local development:
pip install .
pip install ".[full]"
Import Style
Import from the package root only:
from aniket_tools import (
ApiError,
ErrorHandler,
ExceptionHandler,
PaginationRes,
create_response,
explain_error,
get_logger,
get_status_code,
handle_exception,
logs,
unified_exception_handler,
value_correction,
)
Quick Start
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from aniket_tools import create_response, unified_exception_handler
app = FastAPI()
app.add_exception_handler(HTTPException, unified_exception_handler)
app.add_exception_handler(Exception, unified_exception_handler)
app.add_exception_handler(RequestValidationError, unified_exception_handler)
@app.get("/health")
async def health():
return create_response(200, data={"status": "ok"})
Public API
create_response
Builds the standard success or error payload.
Params:
response_code: intdata: Any = Noneschema: Any | None = Nonepagination: Mapping[str, Any] | PaginationRes | None = Noneerror_message: str | None = Noneerror_code: str | None = Nonedetails: Sequence[Mapping[str, Any]] | None = Nonemeta: Mapping[str, Any] | None = Noneas_json_response: bool = True
Usage:
from aniket_tools import create_response
return create_response(
200,
data={"name": "Aniket"},
pagination={"page": 1, "rows": 10, "total_rows": 100},
meta={"request_id": "req-1", "trace_id": "trace-1"},
)
Error usage:
return create_response(
404,
error_message="Report not found.",
error_code="report_not_found",
)
Pagination usage with a shared library model:
from aniket_tools import PaginationRes, create_response
return create_response(
200,
data=rows,
pagination=PaginationRes(page=1, rows=25, total_rows=250),
)
Pagination usage with a dictionary:
return create_response(
200,
data=rows,
pagination={
"page": 1,
"rows": 25,
"total_rows": 250,
},
)
PaginationRes
Shared pagination structure for all projects using the library.
Fields:
page: introws: inttotal_rows: int
Behavior in create_response(...):
- these three fields are validated as the required core pagination contract
pagemust be an integer greater than or equal to1rowsmust be an integer greater than or equal to0total_rowsmust be an integer greater than or equal to0- extra pagination metadata is still allowed and preserved
If pagination is invalid, create_response(...) returns a 422 validation-style error response instead of returning a broken payload.
meta
meta is included in every JSON response body produced by create_response(...) and exception payloads built by ErrorHandler.
Typical use:
request_idtrace_idversionpath- feature or tenant context
Behavior:
- in normal responses, pass
meta={...}directly tocreate_response(...) - in exception responses,
build_payload(...)automatically includes request-derived fields likerequest_idandpath 204 No Contentis the only exception because it has no response body
value_correction
Normalizes output values before returning them.
What it currently fixes:
- trims strings
- converts
Decimaltofloat - converts
datetimeanddateto ISO strings - converts
timedeltato string - rounds floats to 2 decimals
- recursively cleans
dict,list,tuple, andset
Params:
data: Any
Usage:
from decimal import Decimal
from aniket_tools import value_correction
cleaned = value_correction({
"amount": Decimal("10.50"),
"name": " demo ",
})
logs
Logs a message using the default logger or a provided logger.
Params:
msg: object = ""type: str = "info"file_name: str | Path | None = Nonelogger: logging.Logger | None = None
Usage:
from aniket_tools import logs
logs("report created", type="info")
logs("database failed", type="error", file_name="logs/app")
get_logger
Returns a configured Python logger.
Params:
name: str = "aniket_tools"file_name: str | Path | None = None
Usage:
from aniket_tools import get_logger
logger = get_logger("my_app", file_name="logs/app.log")
logger.info("started")
ApiError
Custom business error for controlled API failures.
Params:
message: strstatus_code: int = 400code: str = "api_error"details: list[dict[str, Any]] | None = Nonelog_message: str | None = None
Usage:
from aniket_tools import ApiError
raise ApiError(
"Report is not ready.",
status_code=409,
code="report_pending",
details=[{"field": "report_id", "message": "still processing"}],
)
unified_exception_handler
FastAPI exception handler that logs the raw exception and returns the standard JSON error payload.
Params:
requestexc: Exception
Usage:
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from aniket_tools import unified_exception_handler
app = FastAPI()
app.add_exception_handler(HTTPException, unified_exception_handler)
app.add_exception_handler(Exception, unified_exception_handler)
app.add_exception_handler(RequestValidationError, unified_exception_handler)
ExceptionHandler
Route-level helper that converts an exception into a FastAPI HTTPException.
Params:
exc: Exception
Usage:
from aniket_tools import ExceptionHandler
try:
raise ValueError("invalid meter id")
except Exception as exc:
ExceptionHandler(exc)
handle_exception
Returns the standard error payload directly. If FastAPI is installed, it returns a JSONResponse; otherwise it returns a plain dictionary.
Params:
exc: Exceptionrequest = Nonemeta: Mapping[str, Any] | None = None
Usage:
from aniket_tools import handle_exception
payload_or_response = handle_exception(ValueError("invalid input"))
explain_error
Returns the final user-facing message for an exception.
Params:
exc: Exception
Usage:
from aniket_tools import explain_error
message = explain_error(ValueError("invalid input"))
get_status_code
Returns the final HTTP status code for an exception.
Params:
exc: Exception
Usage:
from aniket_tools import get_status_code
status_code = get_status_code(ValueError("invalid input"))
ErrorHandler
Core exception translator class. This is the class to study if you want to change library behavior.
Params:
logger_name: str = "aniket_tools.errors"use_default_message_for_long_errors: bool = True
Usage:
from aniket_tools import ErrorHandler
handler = ErrorHandler(use_default_message_for_long_errors=True)
info = handler.describe(ValueError("invalid input"))
payload = handler.build_payload(
ValueError("invalid input"),
meta={"trace_id": "trace-1"},
)
Standard Response Shape
Success response
{
"success": true,
"response_code": 200,
"meta": {
"request_id": "req-1"
},
"data": {
"status": "ok"
}
}
Error response
{
"success": false,
"response_code": 422,
"error_message": "One or more fields are invalid.",
"error_type": "RequestValidationError",
"meta": {
"request_id": "req-1",
"path": "/reports"
},
"error": {
"code": "validation_error",
"type": "RequestValidationError",
"message": "One or more fields are invalid.",
"details": [
{
"field": "email",
"message": "field required"
}
]
},
"errors": [
{
"field": "email",
"message": "field required"
}
]
}
Why old and new keys exist:
error_messageanderror.messageare aliaseserrorsanderror.detailsare aliases
This keeps older projects working while giving a cleaner nested error object to newer code.
Code Structure
src/aniket_tools/
__init__.py
_compat.py
exceptions.py
logging.py
responses.py
What each file owns:
__init__.py: public export surface_compat.py: optional imports and type compaction helpersresponses.py: response formatting and value normalizationlogging.py: logger creation and logging helpersexceptions.py: exception classification, message translation, status mapping, and payload generation
Deep Exception System Knowledge
This section is for future editing.
Full flow
When an exception reaches FastAPI, the path is:
- FastAPI calls
unified_exception_handler(request, exc) unified_exception_handlercallsErrorHandler.log_exception(...)unified_exception_handlercallsErrorHandler.handle_exception(...)handle_exception(...)callsbuild_payload(...)build_payload(...)callsdescribe(...)describe(...)decides status code, error code, message, and optional detailsbuild_payload(...)merges response meta likerequest_id,path, or customtrace_idbuild_payload(...)converts that decision into the final JSON structure
The most important method in the whole library is ErrorHandler.describe(...).
Internal decision objects
exceptions.py uses ErrorInfo as the normalized internal result:
ErrorInfo(
status_code=422,
code="validation_error",
message="One or more fields are invalid.",
details=[...],
)
That means the code works in two steps:
- step 1: classify any exception into
ErrorInfo - step 2: turn
ErrorInfointo API JSON
This separation matters. If you want to change decision logic, edit describe(...). If you want to change JSON shape, edit build_payload(...).
Deep Edit Guide For exceptions.py
1. Common exceptions
Common Python exceptions are handled near the end of ErrorHandler.describe(...).
Current branch:
if isinstance(exc, (ValueError, TypeError, KeyError, IndexError, AssertionError)):
return ErrorInfo(
status_code=400,
code="bad_request",
message=_safe_client_message(exc, _default_message(400)),
)
What this means:
- these errors are treated as client-side bad input
- status code is
400 - message is the exception text if it is short and safe
- very long messages fall back to
The request data is invalid.by default - if you create
ErrorHandler(use_default_message_for_long_errors=False), long single-line messages can be preserved
Where to edit common exceptions:
- add or remove exception classes in this branch
- change the code from
bad_requestto another code if needed - change
_safe_client_message(...)behavior if you want stricter or looser exposure of raw messages
2. HTTP exceptions
HTTP exceptions are handled before generic exceptions.
Current logic:
- keep the original status code
- if status is
500or higher, hide raw detail and use a default safe message - if status is
400to499and detail is a short string, use it - if the detail is very long, the default safe message is used unless
use_default_message_for_long_errors=False
This is important because server errors should not leak raw internal detail to the client.
Where to edit:
ErrorHandler.describe(...)in theHTTP_EXCEPTION_TYPESbranch_clean_message(...)if you want to change what counts as safe user text
3. Validation exceptions
Validation is special because it is not just one message. It also needs field-level detail.
Current validation flow:
describe(...)detectsRequestValidationErrororPydanticValidationError- it returns status
422 - it sets code
validation_error - it calls
_normalize_validation_errors(...)
_normalize_validation_errors(...) does three important things:
- reads
exc.errors() - removes framework prefixes like
body,query, andpath - turns nested location tuples into a flat field path like
user.email
Example input from FastAPI:
{"loc": ("body", "email"), "msg": "field required"}
Normalized output:
{"field": "email", "message": "field required"}
Where to edit validation behavior:
- edit
_normalize_validation_errors(...)to change field formatting - edit the validation branch in
describe(...)to change status, code, or top-level message - edit
build_payload(...)if you want validation details under a different payload key
When you should edit validation separately from common errors:
- when frontend needs field-wise messages
- when request body, query, or path errors need custom display
- when nested models should expose a different field path format
4. Database exceptions
Database exceptions are the deepest part of the library.
Current target scope:
- PostgreSQL
- TimescaleDB, which is PostgreSQL-based
- MySQL
- SQLAlchemy-wrapped driver errors from those databases
They are split into categories before ErrorHandler is even used:
INTEGRITY_EXCEPTION_TYPESDATA_EXCEPTION_TYPESDB_UNAVAILABLE_EXCEPTION_TYPESPROGRAMMING_EXCEPTION_TYPESGENERIC_DATABASE_EXCEPTION_TYPES
This is why database logic is cleaner than having one huge if "duplicate key" in str(exc) block.
How database messages are built
_database_message(exc) is the main user-message translator for database failures.
Its flow is:
- prefer
exc.origwhen SQLAlchemy wraps the real driver error - convert the message to lowercase for pattern matching
- extract SQL text when available
- try specific parsers before generic fallback text
Current order inside _database_message(...) matters:
- enum parsing
- argument type mismatch parsing
- duplicate key handling
- foreign key handling
- not-null handling
- check constraint handling
- SQL programming error handling
- database unavailable handling
- generic type mismatch heuristics
- asyncpg-specific fallback
- final generic database message
That order is intentional. The more specific cases must come first.
Argument type mismatch parsing
_describe_argument_type_error(...) handles errors like:
invalid input for query argument $2expected int, got str
It tries to turn database parameter numbers into real column names.
Example SQL:
INSERT INTO billing (site_id, amount) VALUES ($1, $2)
If the driver says argument $2, _extract_insert_column_name(...) maps $2 to amount.
That is why the final user message can become:
Invalid data type for column 'amount': expected int, got str.
instead of a low-level driver message.
Where to edit:
_describe_argument_type_error(...)to support new driver patterns_extract_insert_column_name(...)if you want broader SQL parsing_extract_sql_text(...)if your wrapped DB errors store SQL in a different place
Enum parsing
_describe_enum_error(...) handles raw messages like:
invalid input value for enum status_enum: "donee"
It turns them into cleaner guidance.
Where to edit:
_describe_enum_error(...)
Constraint errors
Constraint messages are handled inside _database_message(...) and _database_status(...).
Current mapping:
- duplicate key -> status
409, codeduplicate_resource - foreign key -> status
422, codeinvalid_reference - not-null -> status
422, codemissing_required_field - check constraint -> status
422, codeconstraint_violation
This split is important:
_database_message(...)decides the text shown to the user_database_status(...)decides the HTTP code and machine-readable error code
If you change one without the other, you can create inconsistent behavior.
Database unavailable errors
Unavailable DB errors currently map to 503, except TimeoutError, which maps to 504.
Where to edit:
DB_UNAVAILABLE_EXCEPTION_TYPES_database_message(...)_database_status(...)- the explicit
TimeoutErrorbranch indescribe(...)
Programming DB errors
Programming errors are server-side query problems, not user-input problems.
Examples:
- undefined table
- undefined column
- SQL syntax error
These currently return a safe message like:
The query referenced a table that does not exist.The query referenced a column that does not exist.SQL syntax error: please verify your query structure.
And they map to 500 with code database_programming_error.
This is correct because these are developer issues, not client data issues.
5. Fallback internal errors
If no branch matches, the library returns:
- status
500 - code
internal_error - message
An unexpected error occurred.
This is the last safety net.
Only change this if you want a different global default.
How To Change Exception Responses Safely
Change only the message
Edit one of these:
_database_message(...)_safe_client_message(...)_normalize_validation_errors(...)- the relevant branch in
ErrorHandler.describe(...)
Use this when you want the same status code and same response shape, but different user text.
Change only the status code
Edit one of these:
_database_status(...)- the relevant branch in
ErrorHandler.describe(...)
Use this when the payload shape is correct but the HTTP semantics are wrong.
Change only the JSON payload shape
Edit one of these:
ErrorHandler.build_payload(...)create_response(...)inresponses.py
Use this when frontend or consumers need extra keys like request_id, trace_id, a standard meta object, or a different nested error structure.
Change response formatting for normal non-exception routes
Edit create_response(...) in responses.py.
That function controls:
- success payloads
- pagination payloads
- error payload aliases
- optional schema validation for response data
Important distinction:
create_response(...)is for normal route returnsunified_exception_handler(...)is for exceptions
They are related, but they are not the same code path.
responses.py Deep Knowledge
value_correction(...)
This is a recursive normalizer.
If you want to support more output types, add them here.
Safe future edits:
- UUID -> string
- Path -> string
- custom dataclass -> dictionary
create_response(...)
This function has five main jobs:
- build success responses
- build direct error responses
- validate response data against a schema when provided
- validate the core pagination structure when
paginationis present - attach a
metaobject to every JSON response body
Deep flow:
- set
successfrom the HTTP status range - set
response_code - normalize
metainto a dictionary - serialize
datathroughvalue_correction(...) - if
schemaexists, validate before storing data - if response-schema validation fails, convert it into a
422payload - if
paginationexists, validatepage,rows, andtotal_rows - if pagination validation fails, convert it into a
422payload - if
error_messageexists or status is>= 400, build theerrorobject - return
JSONResponseor raw dictionary depending onas_json_response
Where to edit:
_serialize_meta(...)for response metadata rulesvalue_correction(...)for data normalization_format_validation_errors(...)for response-schema validation detail formatting_serialize_pagination(...)for pagination rules_set_error_aliases(...)if you want to change legacy keyscreate_response(...)itself for top-level shape or new optional keys
logging.py Deep Knowledge
logging.py is intentionally small.
get_logger(...):
- ensures one stream handler exists
- optionally creates a file handler
- reuses file handlers through
_FILE_HANDLERSso repeated calls do not duplicate log lines
logs(...):
- converts text level like
"error"into a real logging level - delegates to a logger returned by
get_logger(...)
If you see duplicate log lines in future, check _FILE_HANDLERS and handler attachment first.
Safe Editing Rules
When updating the library, keep these rules:
- put specific exception checks before generic ones
- keep validation handling separate from common
400errors - keep database message logic in
_database_message(...) - keep database status logic in
_database_status(...) - keep JSON shape logic in
build_payload(...)orcreate_response(...) - if you add a new public function, also export it from
src/aniket_tools/__init__.py
Reading Order For New Contributors
If you want to understand the code quickly, read in this order:
src/aniket_tools/__init__.pysrc/aniket_tools/responses.pysrc/aniket_tools/logging.pysrc/aniket_tools/exceptions.py
Inside exceptions.py, read in this order:
- grouped exception types
ErrorInfoApiError_normalize_validation_errors(...)_database_message(...)_database_status(...)ErrorHandler.describe(...)ErrorHandler.build_payload(...)unified_exception_handler(...)
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aniket_tools-0.1.2.tar.gz.
File metadata
- Download URL: aniket_tools-0.1.2.tar.gz
- Upload date:
- Size: 27.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96edf64399d905430a61ceea8f18eff89947ad51bf2f21e9071d0e794f78cc42
|
|
| MD5 |
565308c60f87d704328d12406f7ff798
|
|
| BLAKE2b-256 |
46e2123bf2ed789ef55ba3b417950d33bfe371d3642a944fefeb4b67d6949fd3
|
Provenance
The following attestation bundles were made for aniket_tools-0.1.2.tar.gz:
Publisher:
publish-pypi.yml on aniketmodi123/reusable_code_lib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aniket_tools-0.1.2.tar.gz -
Subject digest:
96edf64399d905430a61ceea8f18eff89947ad51bf2f21e9071d0e794f78cc42 - Sigstore transparency entry: 1199598134
- Sigstore integration time:
-
Permalink:
aniketmodi123/reusable_code_lib@f4bf63c57307e6cb0dda1415fdc48eb27acb5cea -
Branch / Tag:
refs/heads/prod - Owner: https://github.com/aniketmodi123
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f4bf63c57307e6cb0dda1415fdc48eb27acb5cea -
Trigger Event:
pull_request
-
Statement type:
File details
Details for the file aniket_tools-0.1.2-py3-none-any.whl.
File metadata
- Download URL: aniket_tools-0.1.2-py3-none-any.whl
- Upload date:
- Size: 19.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e062de630d301b50b841356aeaa6f010f8b94dbca6c227a6e6bb9b392e2d462d
|
|
| MD5 |
bf948cf7bda5036c44aca0b793cb3253
|
|
| BLAKE2b-256 |
9bd0509de3466d674009193aaae248a6b0a4b829aee1ff0aaff62e1ee81cded5
|
Provenance
The following attestation bundles were made for aniket_tools-0.1.2-py3-none-any.whl:
Publisher:
publish-pypi.yml on aniketmodi123/reusable_code_lib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aniket_tools-0.1.2-py3-none-any.whl -
Subject digest:
e062de630d301b50b841356aeaa6f010f8b94dbca6c227a6e6bb9b392e2d462d - Sigstore transparency entry: 1199598197
- Sigstore integration time:
-
Permalink:
aniketmodi123/reusable_code_lib@f4bf63c57307e6cb0dda1415fdc48eb27acb5cea -
Branch / Tag:
refs/heads/prod - Owner: https://github.com/aniketmodi123
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f4bf63c57307e6cb0dda1415fdc48eb27acb5cea -
Trigger Event:
pull_request
-
Statement type: