diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 782fa23..aade22d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 │ # └───────────────────────────────────────────────────┘ diff --git a/encryption.py b/encryption.py index da45e81..b7a4255 100644 --- a/encryption.py +++ b/encryption.py @@ -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", []) @@ -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")) @@ -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]: diff --git a/pyproject.toml b/pyproject.toml index efcf061..1ed8bd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]] @@ -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 diff --git a/src/ledgerbase/config.py b/src/ledgerbase/config.py index 813e264..48a5e7f 100644 --- a/src/ledgerbase/config.py +++ b/src/ledgerbase/config.py @@ -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 { @@ -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]] = { diff --git a/src/ledgerbase/error_handlers.py b/src/ledgerbase/error_handlers.py index c55fef8..20bf56f 100644 --- a/src/ledgerbase/error_handlers.py +++ b/src/ledgerbase/error_handlers.py @@ -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 @@ -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 @@ -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(): diff --git a/src/ledgerbase/models.py b/src/ledgerbase/models.py index 85907be..8732722 100644 --- a/src/ledgerbase/models.py +++ b/src/ledgerbase/models.py @@ -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) diff --git a/src/ledgerbase/security.py b/src/ledgerbase/security.py index 03ff900..7cd3d4d 100644 --- a/src/ledgerbase/security.py +++ b/src/ledgerbase/security.py @@ -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. """ @@ -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. """ @@ -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. """ @@ -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. """ @@ -95,7 +90,6 @@ def configure_logging(app: Flask) -> None: """Configure logging for the Flask app. Args: - ---- app (Flask): The Flask application instance. """ diff --git a/src/services/plaid_service.py b/src/services/plaid_service.py index 673ef85..e587b5c 100644 --- a/src/services/plaid_service.py +++ b/src/services/plaid_service.py @@ -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}" @@ -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 = { @@ -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. """ @@ -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] = { @@ -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. """