Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Make sure we support python 3.8 #82

Merged
merged 6 commits into from
Oct 3, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v3
Expand Down
13 changes: 7 additions & 6 deletions actual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import warnings
import zipfile
from os import PathLike
from typing import IO, Union
from typing import IO, List, Union

from sqlalchemy import insert, update
from sqlmodel import MetaData, Session, create_engine, select
Expand Down Expand Up @@ -136,7 +136,7 @@ def set_file(self, file_id: Union[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]):
"""Runs the migration files, skipping the ones that have already been run. The files can be retrieved from
.data_file_index() method. This first file is the base database, and the following files are migrations.
Migrations can also be .js files. In this case, we have to extract and execute queries from the standard JS."""
Expand Down Expand Up @@ -273,7 +273,7 @@ def reupload_budget(self):
self.reset_user_file(self._file.file_id)
self.upload_budget()

def apply_changes(self, messages: list[Message]):
def apply_changes(self, messages: List[Message]):
"""Applies a list of sync changes, based on what the sync method returned on the remote."""
if not self.engine:
raise UnknownFileId("No valid file available, download one with download_budget()")
Expand Down Expand Up @@ -312,7 +312,8 @@ def update_metadata(self, patch: dict):
then be merged on the metadata and written again to a file."""
metadata_file = self._data_dir / "metadata.json"
if metadata_file.is_file():
config = self.get_metadata() | patch
config = self.get_metadata()
config.update(patch)
else:
config = patch
metadata_file.write_text(json.dumps(config, separators=(",", ":")))
Expand Down Expand Up @@ -423,7 +424,7 @@ def run_rules(self):
transactions = get_transactions(self.session, is_parent=True)
ruleset.run(transactions)

def _run_bank_sync_account(self, acct: Accounts, start_date: datetime.date) -> list[Transactions]:
def _run_bank_sync_account(self, acct: Accounts, start_date: datetime.date) -> List[Transactions]:
sync_method = acct.account_sync_source
account_id = acct.account_id
requisition_id = acct.bank.bank_id if sync_method == "goCardless" else None
Expand Down Expand Up @@ -458,7 +459,7 @@ def _run_bank_sync_account(self, acct: Accounts, start_date: datetime.date) -> l

def run_bank_sync(
self, account: str | Accounts | None = None, start_date: datetime.date | None = None
) -> list[Transactions]:
) -> List[Transactions]:
"""
Runs the bank synchronization for the selected account. If missing, all accounts are synchronized. If a
start_date is provided, is used as a reference, otherwise, the last timestamp of each account will be used. If
Expand Down
2 changes: 1 addition & 1 deletion actual/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def headers(self, file_id: str = None, extra_headers: dict = None) -> dict:
if file_id:
headers["X-ACTUAL-FILE-ID"] = file_id
if extra_headers:
headers = headers | extra_headers
headers.update(extra_headers)
return headers

def info(self) -> InfoDTO:
Expand Down
2 changes: 1 addition & 1 deletion actual/api/bank_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class TransactionItem(BaseModel):
)
date: datetime.date
remittance_information_unstructured: str = Field(None, alias="remittanceInformationUnstructured")
remittance_information_unstructured_array: list[str] = Field(
remittance_information_unstructured_array: List[str] = Field(
default_factory=list, alias="remittanceInformationUnstructuredArray"
)
additional_information: Optional[str] = Field(None, alias="additionalInformation")
Expand Down
2 changes: 1 addition & 1 deletion actual/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def convert(self, is_new: bool = True) -> List[Message]:
changes.append(m)
return changes

def changed(self) -> list[str]:
def changed(self) -> List[str]:
"""Returns list of model changed attributes."""
changed_attributes = []
inspr = inspect(self)
Expand Down
3 changes: 2 additions & 1 deletion actual/migrations.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import re
import uuid
import warnings
from typing import List


def js_migration_statements(js_file: str) -> list[str]:
def js_migration_statements(js_file: str) -> List[str]:
queries = []
matches = re.finditer(r"db\.(execQuery|runQuery)", js_file)
for match in matches:
Expand Down
5 changes: 3 additions & 2 deletions actual/protobuf_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import base64
import datetime
import uuid
from typing import List

import proto

Expand Down Expand Up @@ -120,7 +121,7 @@ def set_timestamp(self, client_id: str = None, now: datetime.datetime = None) ->
def set_null_timestamp(self, client_id: str = None) -> str:
return self.set_timestamp(client_id, datetime.datetime(1970, 1, 1, 0, 0, 0, 0))

def set_messages(self, messages: list[Message], client: HULC_Client, master_key: bytes = None):
def set_messages(self, messages: List[Message], client: HULC_Client, master_key: bytes = None):
if not self.messages:
self.messages = []
for message in messages:
Expand All @@ -146,7 +147,7 @@ class SyncResponse(proto.Message):
messages = proto.RepeatedField(MessageEnvelope, number=1)
merkle = proto.Field(proto.STRING, number=2)

def get_messages(self, master_key: bytes = None) -> list[Message]:
def get_messages(self, master_key: bytes = None) -> List[Message]:
messages = []
for message in self.messages: # noqa
if message.isEncrypted:
Expand Down
10 changes: 5 additions & 5 deletions actual/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def match_transaction(
payee: str | Payees = "",
amount: decimal.Decimal | float | int = 0,
imported_id: str | None = None,
already_matched: list[Transactions] = None,
already_matched: typing.List[Transactions] = None,
) -> typing.Optional[Transactions]:
"""Matches a transaction with another transaction based on the fuzzy matching described at `reconcileTransactions`:

Expand All @@ -130,7 +130,7 @@ def match_transaction(
query = _transactions_base_query(
s, date - datetime.timedelta(days=7), date + datetime.timedelta(days=8), account=account
).filter(Transactions.amount == round(amount * 100))
results: list[Transactions] = s.exec(query).all() # noqa
results: typing.List[Transactions] = s.exec(query).all() # noqa
# filter out the ones that were already matched
if already_matched:
matched = {t.id for t in already_matched}
Expand Down Expand Up @@ -274,7 +274,7 @@ def reconcile_transaction(
cleared: bool = False,
imported_payee: str = None,
update_existing: bool = True,
already_matched: list[Transactions] = None,
already_matched: typing.List[Transactions] = None,
) -> Transactions:
"""Matches the transaction to an existing transaction using fuzzy matching.

Expand Down Expand Up @@ -606,8 +606,8 @@ def get_ruleset(s: Session) -> RuleSet:
"""
rule_set = list()
for rule in get_rules(s):
conditions = TypeAdapter(list[Condition]).validate_json(rule.conditions)
actions = TypeAdapter(list[Action]).validate_json(rule.actions)
conditions = TypeAdapter(typing.List[Condition]).validate_json(rule.conditions)
actions = TypeAdapter(typing.List[Action]).validate_json(rule.actions)
rs = Rule(conditions=conditions, operation=rule.conditions_op, actions=actions, stage=rule.stage) # noqa
rule_set.append(rs)
return RuleSet(rules=rule_set)
Expand Down
35 changes: 23 additions & 12 deletions actual/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def is_valid(self, operation: ConditionType) -> bool:
# must be BOOLEAN
return operation.value in ("is",)

def validate(self, value: typing.Union[int, list[str], str, None], operation: ConditionType = None) -> bool:
def validate(self, value: typing.Union[int, typing.List[str], str, None], operation: ConditionType = None) -> bool:
if isinstance(value, list) and operation in (ConditionType.ONE_OF, ConditionType.NOT_ONE_OF):
return all(self.validate(v, None) for v in value)
if value is None:
Expand Down Expand Up @@ -148,8 +148,8 @@ def from_field(cls, field: str | None) -> ValueType:


def get_value(
value: typing.Union[int, list[str], str, None], value_type: ValueType
) -> typing.Union[int, datetime.date, list[str], str, None]:
value: typing.Union[int, typing.List[str], str, None], value_type: ValueType
) -> typing.Union[int, datetime.date, typing.List[str], str, None]:
"""Converts the value to an actual value according to the type."""
if value_type is ValueType.DATE:
if isinstance(value, str):
Expand All @@ -168,8 +168,8 @@ def get_value(

def condition_evaluation(
op: ConditionType,
true_value: typing.Union[int, list[str], str, datetime.date, None],
self_value: typing.Union[int, list[str], str, datetime.date, BetweenValue, None],
true_value: typing.Union[int, typing.List[str], str, datetime.date, None],
self_value: typing.Union[int, typing.List[str], str, datetime.date, BetweenValue, None],
options: dict = None,
) -> bool:
"""Helper function to evaluate the condition based on the true_value, value found on the transaction, and the
Expand Down Expand Up @@ -268,7 +268,16 @@ class Condition(pydantic.BaseModel):
]
op: ConditionType
value: typing.Union[
int, float, str, list[str], Schedule, list[BaseModel], BetweenValue, BaseModel, datetime.date, None
int,
float,
str,
typing.List[str],
Schedule,
typing.List[BaseModel],
BetweenValue,
BaseModel,
datetime.date,
None,
]
type: typing.Optional[ValueType] = None
options: typing.Optional[dict] = None
Expand All @@ -284,7 +293,7 @@ def as_dict(self):
ret.pop("options", None)
return ret

def get_value(self) -> typing.Union[int, datetime.date, list[str], str, None]:
def get_value(self) -> typing.Union[int, datetime.date, typing.List[str], str, None]:
return get_value(self.value, self.type)

@pydantic.model_validator(mode="after")
Expand Down Expand Up @@ -350,7 +359,7 @@ class Action(pydantic.BaseModel):
op: ActionType = pydantic.Field(ActionType.SET, description="Action type to apply (default changes a column).")
value: typing.Union[str, bool, int, float, pydantic.BaseModel, None]
type: typing.Optional[ValueType] = None
options: dict[str, typing.Union[str, int]] = None
options: typing.Dict[str, typing.Union[str, int]] = None

def __str__(self) -> str:
if self.op in (ActionType.SET, ActionType.LINK_SCHEDULE):
Expand Down Expand Up @@ -445,13 +454,13 @@ class Rule(pydantic.BaseModel):
automatically.
"""

conditions: list[Condition] = pydantic.Field(
conditions: typing.List[Condition] = pydantic.Field(
..., description="List of conditions that need to be met (one or all) in order for the actions to be applied."
)
operation: typing.Literal["and", "or"] = pydantic.Field(
"and", description="Operation to apply for the rule evaluation. If 'all' or 'any' need to be evaluated."
)
actions: list[Action] = pydantic.Field(..., description="List of actions to apply to the transaction.")
actions: typing.List[Action] = pydantic.Field(..., description="List of actions to apply to the transaction.")
stage: typing.Literal["pre", "post", None] = pydantic.Field(
None, description="Stage in which the rule" "will be evaluated (default None)"
)
Expand Down Expand Up @@ -564,7 +573,7 @@ class RuleSet(pydantic.BaseModel):
>>> ])
"""

rules: list[Rule]
rules: typing.List[Rule]

def __str__(self):
return "\n".join([str(r) for r in self.rules])
Expand All @@ -573,7 +582,9 @@ def __iter__(self) -> typing.Iterator[Rule]:
return self.rules.__iter__()

def _run(
self, transaction: typing.Union[Transactions, list[Transactions]], stage: typing.Literal["pre", "post", None]
self,
transaction: typing.Union[Transactions, typing.List[Transactions]],
stage: typing.Literal["pre", "post", None],
):
for rule in [r for r in self.rules if r.stage == stage]:
if isinstance(transaction, list):
Expand Down
11 changes: 7 additions & 4 deletions actual/schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class Schedule(pydantic.BaseModel):
start: datetime.date = pydantic.Field(..., description="Start date of the schedule.")
interval: int = pydantic.Field(1, description="Repeat every interval at frequency unit.")
frequency: Frequency = pydantic.Field(Frequency.MONTHLY, description="Unit for the defined interval.")
patterns: list[Pattern] = pydantic.Field(default_factory=list)
patterns: typing.List[Pattern] = pydantic.Field(default_factory=list)
skip_weekend: bool = pydantic.Field(
False, alias="skipWeekend", description="If should move schedule before or after a weekend."
)
Expand Down Expand Up @@ -192,10 +192,13 @@ def rruleset(self) -> rruleset:
# for the month or weekday rules, add a different rrule to the ruleset. This is because otherwise the rule
# would only look for, for example, days that are 15 that are also Fridays, and that is not desired
if by_month_day:
monthly_config = config.copy() | {"bymonthday": by_month_day}
monthly_config = config.copy()
monthly_config.update({"bymonthday": by_month_day})
rule_sets_configs.append(monthly_config)
if by_weekday:
rule_sets_configs.append(config.copy() | {"byweekday": by_weekday})
weekly_config = config.copy()
weekly_config.update({"byweekday": by_weekday})
rule_sets_configs.append(weekly_config)
# if ruleset does not contain multiple rules, add the current rule as default
if not rule_sets_configs:
rule_sets_configs.append(config)
Expand Down Expand Up @@ -235,7 +238,7 @@ def before(self, date: datetime.date = None) -> typing.Optional[datetime.date]:
return None
return with_weekend_skip.date()

def xafter(self, date: datetime.date = None, count: int = 1) -> list[datetime.date]:
def xafter(self, date: datetime.date = None, count: int = 1) -> typing.List[datetime.date]:
if not date:
date = datetime.date.today()
# dateutils only accepts datetime for evaluation
Expand Down
5 changes: 3 additions & 2 deletions actual/utils/title.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from typing import List

conjunctions = [
"for",
Expand Down Expand Up @@ -159,7 +160,7 @@
)


def convert_to_regexp(special_characters: list[str]):
def convert_to_regexp(special_characters: List[str]):
return [(re.compile(rf"\b{s}\b", re.IGNORECASE), s) for s in special_characters]


Expand All @@ -184,7 +185,7 @@ def replace_func(m: re.Match):
return (lead or "") + (lower or forced or "").upper() + (rest or "")


def title(title_str: str, custom_specials: list[str] = None):
def title(title_str: str, custom_specials: List[str] = None):
title_str = title_str.lower()
title_str = regex.sub(replace_func, title_str)

Expand Down
1 change: 1 addition & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def test_create_user_file(actual_server):
assert len(actual.list_user_files().data) == 0
actual.create_budget("My Budget")
actual.upload_budget()
assert "userId" in actual.get_metadata()
# add some entries to the budget
acct = get_or_create_account(actual.session, "Bank")
assert acct.balance == 0
Expand Down
Loading