Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ repos:
- MD051
- --fix

- repo: https://github.com/jsh9/pydoclint
rev: 88d83c94156c5e51a09938e77019f2c58e92ab58 # pragma: allowlist secret
hooks:
- id: pydoclint
args: ["--config=pyproject.toml"]
types: [python]
stages: [pre-commit]
exclude: ^(tests|scripts|benchmarks|tools)/

# ┌───────────────────────────────────────────────────┐
# │ Your local custom validation scripts │
# └───────────────────────────────────────────────────┘
Expand Down
55 changes: 24 additions & 31 deletions encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,26 @@ class DecryptionError(ValueError):


class Encryptor:
"""Encrypts and decrypts string values using Fernet keys from app config."""
"""Encrypt and decrypt string values using Fernet keys from app config.

CONFIG_ERROR_MSG = (
Initializes the Encryptor with encryption keys from the Flask app config.

Attributes:
CONFIG_ERROR_MSG (str): Error message raised when the secret keys config
is missing or empty.

Raises:
ValueError: If no LEDGERBASE_SECRET_KEYS are found in the config or
if the keys list is empty.

"""

CONFIG_ERROR_MSG: str = (
"Configuration error: 'LEDGERBASE_SECRET_KEYS' must be a non-empty "
"list in Flask config."
)

def __init__(self) -> None:
"""Initialize the Encryptor with encryption keys from the app config.

Raises
------
ValueError
If no LEDGERBASE_SECRET_KEYS are found in the config or
if the keys list is empty.

"""
config = cast("AppConfig", current_app.config)

keys: list[str] = config.get("LEDGERBASE_SECRET_KEYS", [])
Expand All @@ -72,15 +75,11 @@ def __init__(self) -> None:
def encrypt(self, value: str) -> str:
"""Encrypt a string value using the primary key.

Parameters
----------
value : str
The string value to encrypt.
Args:
value (str): The string value to encrypt.

Returns
-------
str
The encrypted string.
Returns:
str: The encrypted string.

"""
encrypted_bytes: bytes = self.primary_cipher.encrypt(value.encode("utf-8"))
Expand All @@ -89,20 +88,14 @@ def encrypt(self, value: str) -> str:
def decrypt(self, token: str) -> str:
"""Decrypt a token using the primary key, then fall back to secondary keys.

Parameters
----------
token : str
The encrypted string token.
Args:
token (str): The encrypted string token.

Returns
-------
str
The original decrypted string.
Returns:
str: The original decrypted string.

Raises
------
DecryptionError
If decryption fails with all known keys.
Raises:
DecryptionError: If decryption fails with all known keys.

"""
for cipher in [self.primary_cipher, *self.secondary_ciphers]:
Expand Down
11 changes: 11 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pydantic = "^2.9.0,<2.10.0"
poetry-plugin-export = "^1.8.0"
PyGithub = "^1.59.0"
contourpy = "^1.3.2"
pydoclint = ">=0.8.4"


[[tool.poetry.source]]
Expand Down Expand Up @@ -200,3 +201,13 @@ ignore-words-list = "ledgernase, PII, mycustomterm" # Add your words here
check-filenames = true
# Check hidden files
check-hidden = true

[tool.pydoclint]
style = "google"
exclude = '\.git|tests/|scripts/|benchmarks/|tools/|noxfile\.py|\.claude/'
arg-type-hints-in-docstring = true
arg-type-hints-in-signature = true
skip-checking-raises = false
require-return-section-when-returning-nothing = false
require-return-section-when-returning-values = false
require-yield-section-when-yielding-values = false
8 changes: 4 additions & 4 deletions src/ledgerbase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def get_security_settings() -> dict[str, Any]:
These settings can be extended to read from secure storage.

Returns:
Dictionary with SESSION_COOKIE_SECURE and PREFERRED_URL_SCHEME.
dict[str, Any]: Dictionary with SESSION_COOKIE_SECURE and PREFERRED_URL_SCHEME.

"""
return {
Expand All @@ -83,11 +83,11 @@ def get_config(env: str | None = None) -> type[Config]:
"""Select a Config subclass based on the given environment.

Args:
env: One of 'development', 'production', or None. If None, reads
the FLASK_ENV environment variable (defaults to 'development').
env (str | None): One of 'development', 'production', or None. If None,
reads the FLASK_ENV environment variable (defaults to 'development').

Returns:
The Config subclass corresponding to the environment.
type[Config]: The Config subclass corresponding to the environment.

"""
mapping: dict[str, type[Config]] = {
Expand Down
9 changes: 9 additions & 0 deletions src/ledgerbase/error_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def handle_validation_error(
Args:
error (ValidationError): The validation error instance.

Returns:
Response | str | tuple[Response | str, int]: A JSON or HTML 422 response.

"""
if _wants_json():
return jsonify({"errors": error.messages}), 422
Expand All @@ -56,6 +59,9 @@ def handle_not_found(
Args:
_error (NotFound): The exception instance (unused).

Returns:
Response | str | tuple[Response | str, int]: A JSON or HTML 404 response.

"""
if _wants_json():
return jsonify({"error": "Not found"}), 404
Expand All @@ -70,6 +76,9 @@ def handle_internal_error(
Args:
error (Exception): The exception instance.

Returns:
Response | str | tuple[Response | str, int]: A JSON or HTML 500 response.

"""
current_app.logger.exception("Unhandled exception occurred: %s", error)
if _wants_json():
Expand Down
4 changes: 2 additions & 2 deletions src/ledgerbase/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ class ExampleModel(db.Model):

"""

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
id: int = db.Column(db.Integer, primary_key=True)
name: str = db.Column(db.String(50), nullable=False)
Comment on lines +40 to +41
8 changes: 1 addition & 7 deletions src/ledgerbase/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def apply_secure_headers(app: Flask) -> None:
"""Apply secure headers to all responses in the Flask app.

Args:
----
app (Flask): The Flask application instance.

"""
Expand All @@ -40,11 +39,9 @@ def set_secure_headers(response: Response) -> Response:
"""Set secure headers for the response.

Args:
----
response (Response): The Flask response object.

Returns:
-------
Response: The modified response with secure headers.

"""
Expand Down Expand Up @@ -72,7 +69,6 @@ def configure_rate_limiting(app: Flask) -> None:
"""Configure rate limiting for the Flask app.

Args:
----
app (Flask): The Flask application instance.

"""
Expand All @@ -83,8 +79,7 @@ def configure_rate_limiting(app: Flask) -> None:
def login() -> str:
"""Handle login attempts with rate limiting.

Returns
-------
Returns:
str: A message indicating a login attempt.

"""
Expand All @@ -95,7 +90,6 @@ def configure_logging(app: Flask) -> None:
"""Configure logging for the Flask app.

Args:
----
app (Flask): The Flask application instance.

"""
Expand Down
34 changes: 12 additions & 22 deletions src/services/plaid_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,12 @@ def plaid_request(endpoint: str, payload: dict[str, Any]) -> dict[str, Any] | No
"""Make a request to the Plaid API.

Args:
----
endpoint (str): The API endpoint to call.
payload (Dict[str, Any]): The payload to send in the request.
payload (dict[str, Any]): The payload to send in the request.

Returns:
-------
Optional[Dict[str, Any]]: The JSON response from the API,
or None if the request fails. # noqa: E501
dict[str, Any] | None: The JSON response from the API,
or None if the request fails.

"""
url = f"{BASE_URL}{endpoint}"
Expand Down Expand Up @@ -63,13 +61,11 @@ def create_link_token(user_id: str = "user-unique-id") -> dict[str, Any] | None:
"""Create a link token for the Plaid API.

Args:
----
user_id (str): A unique identifier for the user. Defaults to "user-unique-id".

Returns:
-------
Optional[Dict[str, Any]]: The response containing the link token,
or None if the request fails. # noqa: E501
dict[str, Any] | None: The response containing the link token,
or None if the request fails.

"""
payload = {
Expand All @@ -86,12 +82,10 @@ def get_accounts(access_token: str) -> dict[str, Any] | None:
"""Retrieve account information from the Plaid API.

Args:
----
access_token (str): The access token for the user's account.

Returns:
-------
Optional[Dict[str, Any]]: The response containing account information,
dict[str, Any] | None: The response containing account information,
or None if the request fails.

"""
Expand All @@ -108,17 +102,15 @@ def get_transactions(
"""Retrieve transaction data from the Plaid API.

Args:
----
access_token (str): The access token for the user's account.
start_date (str): The start date for the transaction query (YYYY-MM-DD).
end_date (str): The end date for the transaction query (YYYY-MM-DD).
options (Optional[Dict[str, Any]]): Additional options for the query.
Defaults to None. # noqa: E501
options (dict[str, Any] | None): Additional options for the query.
Defaults to None.

Returns:
-------
Optional[Dict[str, Any]]: The response containing transaction data, or
None if the request fails.
dict[str, Any] | None: The response containing transaction data,
or None if the request fails.

"""
payload: dict[str, Any] = {
Expand All @@ -138,13 +130,11 @@ def sync_transactions(
"""Synchronize transactions using the Plaid API.

Args:
----
access_token (str): The access token for the user's account.
cursor (Optional[str]): The cursor for incremental sync. Defaults to None.
cursor (str | None): The cursor for incremental sync. Defaults to None.

Returns:
-------
Optional[Dict[str, Any]]: The response containing synchronized transactions,
dict[str, Any] | None: The response containing synchronized transactions,
or None if the request fails.

"""
Expand Down
Loading