Skip to content

Commit

Permalink
feat: Multiple changes to core files. (#34)
Browse files Browse the repository at this point in the history
- Introduce issue templates for Github (closes #28)
- Fix command to install directly from git (closes #33)
- Add documentation on how to install new transactions and run rules
- Add example on how to import a CSV file
- Small fixes to the Gnucash import
- Add some more test coverage.
  • Loading branch information
bvanelli authored Jul 1, 2024
1 parent d79c930 commit f1e3a18
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 34 deletions.
69 changes: 69 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: '🐛 Bug report'
description: Report an issue with Actual.
labels: [bug]

body:
- type: checkboxes
id: checks
attributes:
label: Checks
options:
- label: I have checked that this issue has not already been reported.
required: true
- label: I have confirmed this bug exists on the latest version of actualpy.
required: true

- type: textarea
id: example
attributes:
label: Reproducible example
description: >
Please follow [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) on how to
provide a minimal, copy-pastable example. Include the (wrong) output if applicable.
value: |
```python
```
validations:
required: true

- type: textarea
id: logs
attributes:
label: Log output
description: >
Include the stack trace, if available, of the problem being reported.
render: shell

- type: textarea
id: problem
attributes:
label: Issue description
description: >
Provide any additional information you think might be relevant. Things like which features are being used
(bank syncs, use case, etc).
validations:
required: true

- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: >
Describe or show a code example of the expected behavior. This might be the relevant UI or code snippet where
Actual will handle things correctly, but the library does not.
validations:
required: true

- type: textarea
id: version
attributes:
label: Installed versions
description: >
Describe which version (or if running to git version, which commit) of the Python library and Actual Server
are being ran.
value: >
- actualpy version:
- Actual Server version:
validations:
required: true
14 changes: 14 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: '✨ Feature request'
description: Suggest a new feature or enhancement for actualpy.
labels: [enhancement]

body:
- type: textarea
id: description
attributes:
label: Description
description: >
Describe the feature or enhancement and explain why it should be implemented.
Include a code example if applicable.
validations:
required: true
91 changes: 69 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Python API implementation for Actual server.
Install it via Pip using the repository url:

```bash
pip install https://github.com/bvanelli/actualpy
pip install git+https://github.com/bvanelli/actualpy.git
```

# Basic usage
Expand All @@ -42,6 +42,38 @@ with Actual(
print(t.date, account_name, t.notes, t.amount, category)
```

## Adding new transactions

After you created your first budget (or when updating an existing budget), you can add new transactions by adding them
using the `actual.session.add()` method. You cannot use the SQLAlchemy session directly because that adds the entries to your
local database, but will not sync the results back to the server (that is only possible when re-uploading the file).

The method will make sure the local database is updated, but will also send a SYNC request with the added data so that
it will be immediately available on the frontend:

```python
import decimal
import datetime
from actual import Actual
from actual.queries import create_transaction, create_account

with Actual(base_url="http://localhost:5006", password="mypass", file="My budget") as actual:
act = create_account(actual.session, "My account")
t = create_transaction(
actual.session,
datetime.date.today(),
act,
"My payee",
notes="My first transaction",
amount=decimal.Decimal(-10.5),
)
actual.commit() # use the actual.commit() instead of session.commit()!
```

Will produce:

![added-transaction](./docs/static/added-transaction.png)

# Experimental features

> **WARNING:** Experimental features do not have all the testing necessary to ensure correctness in comparison to the
Expand All @@ -67,36 +99,51 @@ You will then have a freshly created new budget to use:

If the `encryption_password` is set, the budget will additionally also be encrypted on the upload step to the server.

# Adding new transactions
## Updating transactions using Bank Sync

After you created your first budget (or when updating an existing budget), you can add new transactions by adding them
using the `actual.session.add()` method. You cannot use the SQLAlchemy session directly because that adds the entries to your
local database, but will not sync the results back to the server (that is only possible when re-uploading the file).
If you have either [goCardless](https://actualbudget.org/docs/advanced/bank-sync/#gocardless-setup) or
[simplefin](https://actualbudget.org/docs/experimental/simplefin-sync/) integration configured, it is possible to
update the transactions using just the Python API alone. This is because the actual queries to the third-party service
are handled on the server, so the client does not have to do any custom API queries.

The method will make sure the local database is updated, but will also send a SYNC request with the added data so that
it will be immediately available on the frontend:
To sync your account, simply call the `run_bank_sync` method:

```python
from actual import Actual

with Actual(base_url="http://localhost:5006", password="mypass") as actual:
synchronized_transactions = actual.run_bank_sync()
for transaction in synchronized_transactions:
print(f"Added of modified {transaction}")
# sync changes back to the server
actual.commit()
```

## Running rules

You can also automatically run rules using the library:

```python
import decimal
import datetime
from actual import Actual
from actual.queries import create_transaction, create_account

with Actual(base_url="http://localhost:5006", password="mypass", file="My budget") as actual:
act = create_account(actual.session, "My account")
t = create_transaction(
actual.session,
datetime.date.today(),
act,
"My payee",
notes="My first transaction",
amount=decimal.Decimal(-10.5),
)
actual.session.add(t)
actual.commit() # use the actual.commit() instead of session.commit()!
actual.run_rules()
```

![added-transaction](./docs/static/added-transaction.png)
You can also manipulate the rules individually:

```python
from actual import Actual
from actual.queries import get_ruleset, get_transactions

with Actual(base_url="http://localhost:5006", password="mypass", file="My budget") as actual:
rs = get_ruleset(actual.session)
transactions = get_transactions(actual.session)
for rule in rs:
for t in transactions:
if rule.evaluate(t):
print(f"Rule {rule} matches for {t}")
```

# Understanding how Actual handles changes

Expand Down
23 changes: 19 additions & 4 deletions actual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from actual.queries import (
get_account,
get_accounts,
get_ruleset,
get_transactions,
reconcile_transaction,
)
Expand All @@ -46,6 +47,7 @@ def __init__(
encryption_password: str = None,
data_dir: Union[str, pathlib.Path] = None,
bootstrap: bool = False,
sa_kwargs: dict = None,
):
"""
Implements the Python API for the Actual Server in order to be able to read and modify information on Actual
Expand All @@ -60,8 +62,11 @@ def __init__(
:param file: the name or id of the file to be set
:param encryption_password: password used to configure encryption, if existing
:param data_dir: where to store the downloaded files from the server. If not specified, a temporary folder will
be created instead.
be created instead.
:param bootstrap: if the server is not bootstrapped, bootstrap it with the password.
:param sa_kwargs: additional kwargs passed to the SQLAlchemy session maker. Examples are `autoflush` (enabled
by default), `autocommit` (disabled by default). For a list of all parameters, check the SQLAlchemy
documentation: https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.__init__
"""
super().__init__(base_url, token, password, bootstrap)
self._file: RemoteFileListDTO | None = None
Expand All @@ -75,12 +80,14 @@ def __init__(
self._encryption_password = encryption_password
self._master_key = None
self._in_context = False
self._sa_kwargs = sa_kwargs or {}
if "autoflush" not in self._sa_kwargs:
self._sa_kwargs["autoflush"] = True

def __enter__(self) -> Actual:
self._in_context = True
if self._file:
self.download_budget(self._encryption_password)
self._session = strong_reference_session(Session(self.engine))
self._in_context = True
return self

def __exit__(self, exc_type, exc_val, exc_tb):
Expand Down Expand Up @@ -162,7 +169,7 @@ def create_budget(self, budget_name: str):
# generate a session
self.engine = create_engine(f"sqlite:///{self._data_dir}/db.sqlite")
if self._in_context:
self._session = strong_reference_session(Session(self.engine, autoflush=False))
self._session = strong_reference_session(Session(self.engine, **self._sa_kwargs))
# create a clock
self.load_clock()

Expand Down Expand Up @@ -292,6 +299,9 @@ def download_budget(self, encryption_password: str = None):
# actual js always calls validation
self.validate()
self.sync()
# create session if not existing
if self._in_context and not self._session:
self._session = strong_reference_session(Session(self.engine, **self._sa_kwargs))

def import_zip(self, file_bytes: str | PathLike[str] | IO[bytes]):
try:
Expand Down Expand Up @@ -362,6 +372,11 @@ def commit(self):
# sync all changes to the server
self.sync_sync(req)

def run_rules(self):
ruleset = get_ruleset(self.session)
transactions = get_transactions(self.session)
ruleset.run(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
Expand Down
2 changes: 1 addition & 1 deletion actual/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def reconcile_transaction(
if update_existing:
match.notes = notes
if category:
match.category = get_or_create_category(s, category).id
match.category_id = get_or_create_category(s, category).id
match.set_date(date)
return match
return create_transaction(s, date, account, payee, notes, category, amount, imported_id, cleared, imported_payee)
Expand Down
5 changes: 4 additions & 1 deletion actual/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,9 @@ class RuleSet(pydantic.BaseModel):
def __str__(self):
return "\n".join([str(r) for r in self.rules])

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]
):
Expand All @@ -412,7 +415,7 @@ def _run(

def run(
self,
transaction: typing.Union[Transactions, list[Transactions]],
transaction: typing.Union[Transactions, typing.Sequence[Transactions]],
stage: typing.Literal["all", "pre", "post", None] = "all",
):
"""Runs the rules for each and every transaction on the list. If stage is 'all' (default), all rules are run in
Expand Down
13 changes: 13 additions & 0 deletions examples/csv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
This example shows how to import a CSV file directly into Actual without having to use the UI.

It also reconciles the transactions, to make sure that a transaction cannot be inserted twice into the database.

The file under `files/transactions.csv` is a direct export from Actual and contains the following fields:

- Account
- Date
- Payee
- Notes
- Category
- Amount
- Cleared
19 changes: 19 additions & 0 deletions examples/csv/files/transactions.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Account,Date,Payee,Notes,Category,Amount,Cleared
Current Checking Account,2024-04-01,,Paying rent,Rent,-250,Not cleared
Current Savings Account,2024-01-31,Current Checking Account,Saving money,,200,Cleared
Current Checking Account,2024-01-31,Current Savings Account,Saving money,,-200,Not cleared
Current Checking Account,2024-01-31,,Streaming services,Online Services,-15,Not cleared
Current Checking Account,2024-01-30,,Groceries,Groceries,-15,Not cleared
Current Checking Account,2024-01-26,,New pants,Clothes,-40,Not cleared
Current Checking Account,2024-01-26,,Groceries,Groceries,-25,Not cleared
Current Checking Account,2024-01-19,,Groceries,Groceries,-25,Not cleared
Current Checking Account,2024-01-18,,University book,Books,-30,Not cleared
Current Checking Account,2024-01-16,,Phone contract,Phone,-15,Not cleared
Current Checking Account,2024-01-13,,Cinema tickets,Entertainment:Music/Movies,-10,Not cleared
Current Checking Account,2024-01-12,,Groceries,Groceries,-25,Not cleared
Current Cash in Wallet,2024-01-06,,Couple of beers at a bar,Entertainment:Recreation,-25,Not cleared
Current Cash in Wallet,2024-01-06,Current Checking Account,Cash withdraw,,50,Not cleared
Current Checking Account,2024-01-06,Current Cash in Wallet,Cash withdraw,,-50,Not cleared
Current Checking Account,2024-01-05,,Groceries,Groceries,-25,Not cleared
Current Checking Account,2024-01-01,,Mobility Bonus,Other Income,100,Not cleared
Current Checking Account,2024-01-01,,Salary Payment,Salary,700,Not cleared
Loading

0 comments on commit f1e3a18

Please sign in to comment.