Skip to content
Merged
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
34 changes: 18 additions & 16 deletions actual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import zipfile
from collections.abc import Sequence
from os import PathLike
from typing import IO, cast
from typing import IO, Any, cast

import httpx
from sqlalchemy.engine import Engine
Expand Down Expand Up @@ -71,7 +71,7 @@ def __init__(
data_dir: str | pathlib.Path | None = None,
cert: bool | ssl.SSLContext | str = True,
bootstrap: bool = False,
sa_kwargs: dict | None = None,
sa_kwargs: dict[str, Any] | None = None,
extra_headers: dict[str, str] | None = None,
timeout: float | httpx.Timeout | None = 60.0,
):
Expand Down Expand Up @@ -150,7 +150,7 @@ def __enter__(self) -> Actual:
self.download_budget(self._encryption_password)
return self

def __exit__(self, exc_type, exc_val, exc_tb):
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
if self._session:
self._session.close()
if self.engine:
Expand Down Expand Up @@ -222,7 +222,7 @@ def set_file(self, file_id: str | RemoteFileListDTO) -> RemoteFileListDTO:
raise UnknownFileId(f"Multiple files found with identifier '{file_id}'")
return self.set_file(selected_files[0])

def run_migrations(self, migration_files: list[str]):
def run_migrations(self, migration_files: list[str]) -> None:
"""
Runs the migration files, skipping the ones that have already been run.

Expand Down Expand Up @@ -253,7 +253,7 @@ def run_migrations(self, migration_files: list[str]):
raise ActualError("Engine not initialized. Download or create a budget first.")
self._database_metadata = reflect_model(self.engine)

def create_budget(self, budget_name: str):
def create_budget(self, budget_name: str) -> None:
"""
Creates a budget using the remote server default database and migrations.

Expand Down Expand Up @@ -295,11 +295,11 @@ def create_budget(self, budget_name: str):
get_or_create_clock(session, self._hulc_client)
session.commit()

def rename_budget(self, budget_name: str):
def rename_budget(self, budget_name: str) -> None:
"""Renames the budget with the given name."""
self.update_user_file_name(self.file.file_id, budget_name)

def delete_budget(self):
def delete_budget(self) -> None:
"""Deletes the currently loaded file from the server."""
self.delete_user_file(self.file.file_id)
# reset group id, as file cannot be synced anymore
Expand Down Expand Up @@ -358,7 +358,7 @@ def export_data(self, output_file: str | PathLike[str] | IO[bytes] | None = None
output_file.write(content)
return content

def encrypt(self, encryption_password: str):
def encrypt(self, encryption_password: str) -> None:
"""
Encrypts the local database using a new key, and re-uploads to the server.

Expand Down Expand Up @@ -417,7 +417,7 @@ def upload_budget(self) -> None:
raise ActualEncryptionError("Budget is encrypted but password was not provided")
self.encrypt(self._encryption_password)

def reupload_budget(self):
def reupload_budget(self) -> None:
"""
Resets the user file on the backend and re-uploads the current copy instead.

Expand Down Expand Up @@ -477,13 +477,13 @@ def apply_changes(self, messages: list[Message]) -> list[Changeset]:
s.commit()
return changes

def get_metadata(self) -> dict:
def get_metadata(self) -> dict[str, Any]:
"""Gets the content of the `metadata.json` file."""
metadata_file = self.data_dir / "metadata.json"
# Here, we trust the metadata will have the correct format
return cast(dict, json.loads(metadata_file.read_text()))
return cast(dict[str, Any], json.loads(metadata_file.read_text()))

def update_metadata(self, patch: dict):
def update_metadata(self, patch: dict[str, Any]) -> None:
"""
Updates the `metadata.json` from the Actual file with the patch fields.

Expand All @@ -497,7 +497,7 @@ def update_metadata(self, patch: dict):
config = patch
metadata_file.write_text(json.dumps(config, separators=(",", ":")))

def download_budget(self, encryption_password: str | None = None):
def download_budget(self, encryption_password: str | None = None) -> None:
"""
Downloads the budget file from the remote, applying all following changes required by the server.

Expand Down Expand Up @@ -532,6 +532,8 @@ def download_budget(self, encryption_password: str | None = None):
if self._master_key is None:
raise ActualDecryptionError("Master encryption key is not set.")
file_info = self.get_user_file_info(self.file.file_id)
if file_info.data.encrypt_meta is None:
raise ActualDecryptionError("Encryption metadata is missing on the user file.")
# decrypt file bytes
file_bytes = decrypt_from_meta(self._master_key, file_bytes, file_info.data.encrypt_meta)
self.import_zip(io.BytesIO(file_bytes))
Expand Down Expand Up @@ -563,7 +565,7 @@ def download_master_encryption_key(self, encryption_password: str | None) -> byt
self._master_key = create_key_buffer(encryption_password, key_info.data.salt)
return self._master_key

def import_zip(self, file_bytes: str | PathLike[str] | IO[bytes]):
def import_zip(self, file_bytes: str | PathLike[str] | IO[bytes]) -> None:
"""
Imports a zip file as the current database, as well as generating the local reflected session.

Expand Down Expand Up @@ -628,7 +630,7 @@ def sync(self) -> list[Changeset]:
session.commit()
return changeset

def commit(self):
def commit(self) -> None:
"""
Adds all pending entries done to the local database, and sends a sync request to the remote server.

Expand All @@ -654,7 +656,7 @@ def commit(self):
if self.file.group_id: # only files with a group id can be synced
self.sync_sync(req)

def run_rules(self, transactions: Sequence[Transactions] | None = None):
def run_rules(self, transactions: Sequence[Transactions] | None = None) -> None:
"""Runs all the stored rules on the database on all transactions, without any filters."""
if transactions is None:
transactions = get_transactions(self.session)
Expand Down
4 changes: 2 additions & 2 deletions actual/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def login(
self._token = login_response.data.token
return login_response

def headers(self, file_id: str | None = None, extra_headers: dict | None = None) -> dict:
def headers(self, file_id: str | None = None, extra_headers: dict[str, str] | None = None) -> dict[str, str]:
"""
Generates a header based on the stored token for the connection.

Expand Down Expand Up @@ -280,7 +280,7 @@ def update_user_file_name(self, file_id: str, file_name: str) -> StatusDTO:
response.raise_for_status()
return StatusDTO.model_validate(response.json())

def delete_user_file(self, file_id: str):
def delete_user_file(self, file_id: str) -> StatusDTO:
"""Deletes the user file loaded from the remote server."""
response = self._requests_session.post(
Endpoints.DELETE_USER_FILE.value, json={"fileId": file_id, "token": self._token}
Expand Down
4 changes: 2 additions & 2 deletions actual/api/bank_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class DebtorAccount(BaseModel):
iban: str

@property
def masked_iban(self):
def masked_iban(self) -> str:
return f"({self.iban[:4]} XXX {self.iban[-4:]})"


Expand Down Expand Up @@ -97,7 +97,7 @@ class TransactionItem(BaseModel):
posted_date: datetime.date | None = Field(None, alias="postedDate")

@property
def imported_payee(self):
def imported_payee(self) -> str:
"""Deprecated method to convert the payee name. Use the payee_name instead."""
name_parts = []
name = self.payee or self.notes or self.additional_information
Expand Down
2 changes: 1 addition & 1 deletion actual/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class Endpoints(enum.Enum):
OPEN_ID_ENABLE = "openid/enable"
OPEN_ID_DISABLE = "openid/disable"

def __str__(self):
def __str__(self) -> str:
return self.value


Expand Down
27 changes: 14 additions & 13 deletions actual/budgets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dataclasses
import datetime
import decimal
import typing
from collections.abc import Iterator

from sqlmodel import Session, col, select
Expand Down Expand Up @@ -93,7 +94,7 @@ def carryover(self) -> bool:
return False
return bool(self.budget.carryover)

def as_dict(self) -> dict:
def as_dict(self) -> dict[str, str | bool | decimal.Decimal | None]:
"""
Converts the budget category to a dictionary representation.

Expand Down Expand Up @@ -140,7 +141,7 @@ def accumulated_balance(self) -> decimal.Decimal:
"""Sum of all accumulated balances from the categories under this category group."""
return sum([c.accumulated_balance for c in self.categories], start=decimal.Decimal(0))

def as_dict(self) -> dict:
def as_dict(self) -> dict[str, typing.Any]:
"""
Converts the budget category group to a dictionary representation, including nested categories.

Expand Down Expand Up @@ -183,7 +184,7 @@ class IncomeCategory(_HasDatabaseObject):
If a budget was not set for the month, it will be set to `None`, and budgeted amount assumed to be zero.
"""

def as_dict(self) -> dict:
def as_dict(self) -> dict[str, str | bool | decimal.Decimal | None]:
"""
Converts the income category to a dictionary representation.
"""
Expand Down Expand Up @@ -225,7 +226,7 @@ def budgeted(self) -> decimal.Decimal:
"""
return sum([c.budgeted for c in self.categories], start=decimal.Decimal(0))

def as_dict(self) -> dict:
def as_dict(self) -> dict[str, typing.Any]:
"""
Converts the income category group to a dictionary representation, including nested categories.
"""
Expand Down Expand Up @@ -342,7 +343,7 @@ def available_funds(self) -> decimal.Decimal:
return self.received + self.from_last_month

@property
def to_budget(self):
def to_budget(self) -> decimal.Decimal:
"""
The amount of money available for budgeting.

Expand All @@ -351,7 +352,7 @@ def to_budget(self):
"""
return self.available_funds - self.budgeted - self.for_next_month + self.last_month_overspent

def as_dict(self) -> dict:
def as_dict(self) -> dict[str, typing.Any]:
"""
Converts the envelope budget to a dictionary representation, including all category groups.

Expand Down Expand Up @@ -400,7 +401,7 @@ def budgeted_income(self) -> decimal.Decimal:
"""The amount of income budgeted for the current month."""
return sum([cat.budgeted for cat in self.income_category_groups], start=decimal.Decimal(0))

def as_dict(self) -> dict:
def as_dict(self) -> dict[str, typing.Any]:
"""
Converts the tracking budget to a dictionary representation, including all category groups.

Expand Down Expand Up @@ -434,7 +435,7 @@ class BudgetList(list[EnvelopeBudget | TrackingBudget]):
:param is_tracking_budget: Whether the budgets are tracking budgets (True) or envelope budgets (False).
"""

def __init__(self, iterable, is_tracking_budget: bool = False):
def __init__(self, iterable: typing.Iterable["EnvelopeBudget | TrackingBudget"], is_tracking_budget: bool = False):
super().__init__(iterable)
self.is_tracking_budget: bool = is_tracking_budget

Expand Down Expand Up @@ -489,14 +490,14 @@ def _get_category_detailed_budget(
if not budget:
# create a temporary budget
range_start, range_end = month_range(month)
balance = s.scalar(_balance_base_query(s, range_start, range_end, category=category))
scalar_balance = s.scalar(_balance_base_query(s, range_start, range_end, category=category))
budgeted = decimal.Decimal(0)
spent = cents_to_decimal(balance)
spent = cents_to_decimal(scalar_balance)
else:
budgeted = budget.get_amount()
spent = budget.balance
# Balance is the simple subtraction of the spent from the budget amount
balance = budgeted + spent
balance: decimal.Decimal = budgeted + spent
# The accumulated balance relates to a previous budget
return BudgetCategory(
category,
Expand Down Expand Up @@ -578,7 +579,7 @@ def _calculate_accumulated_balance(
def _process_expense_categories(
s: Session,
month: datetime.date,
category_groups: list[CategoryGroups],
category_groups: typing.Sequence[CategoryGroups],
previous_budget: EnvelopeBudget | TrackingBudget | None,
is_tracking: bool,
) -> list[BudgetCategoryGroup]:
Expand All @@ -595,7 +596,7 @@ def _process_expense_categories(


def _process_income_categories(
s: Session, month: datetime.date, income_category_groups: list[CategoryGroups], is_tracking: bool
s: Session, month: datetime.date, income_category_groups: typing.Sequence[CategoryGroups], is_tracking: bool
) -> list[IncomeCategoryGroup]:
"""Processes all income category groups for a given month."""
income_cat_group_list: list[IncomeCategoryGroup] = []
Expand Down
Loading
Loading