Skip to content

Commit

Permalink
Merge pull request #248 from SFTtech/milo/switch-to-sftkit
Browse files Browse the repository at this point in the history
refactor(core): integrate sftkit
  • Loading branch information
mikonse authored Dec 27, 2024
2 parents cacef74 + b2b510c commit fcddfe1
Show file tree
Hide file tree
Showing 57 changed files with 293 additions and 4,719 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
ABRECHNUNG_SERVICE__URL=http://localhost:8080
ABRECHNUNG_SERVICE__API_URL=https://localhost:8080/api
ABRECHNUNG_SERVICE__NAME=Abrechnung
ABRECHNUNG_DATABASE__HOST=abrechnung_postgres
ABRECHNUNG_DATABASE__USER=abrechnung
ABRECHNUNG_DATABASE__DBNAME=abrechnung
ABRECHNUNG_DATABASE__PASSWORD=replaceme # e.g. pwgen -s 64 1

ABRECHNUNG_API__SECRET_KEY=replaceme # pwgen -s 64 1
ABRECHNUNG_SERVICE__BASE_URL=https://localhost:8080
ABRECHNUNG_API__PORT=8080
ABRECHNUNG_API__ID=default

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/backend.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ jobs:
strategy:
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
os: [ubuntu-latest]
services:
postgres:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

[Compare the full difference.](https://github.com/SFTtech/abrechnung/compare/v0.14.0...HEAD)

**BREAKING**
- changed config structure to only include the reverse-proxy base_url once in the `api` section
- drop python 3.10 support

**OTHER**
- add Spanish and Tamil as supported languages
- rework group settings to just be a single page
- allow archiving of groups
Expand Down
5 changes: 3 additions & 2 deletions abrechnung/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from abrechnung.application.users import UserService
from abrechnung.config import Config
from abrechnung.framework.database import create_db_pool
from abrechnung.database.migrations import get_database

logger = logging.getLogger(__name__)

Expand All @@ -16,7 +16,8 @@ async def create_user(config: Config, name: str, email: str, skip_email_check: b
print("Passwords do not match!")
return

db_pool = await create_db_pool(config.database)
database = get_database(config.database)
db_pool = await database.create_pool()
user_service = UserService(db_pool, config)
user_service.enable_registration = True
if skip_email_check:
Expand Down
36 changes: 19 additions & 17 deletions abrechnung/application/accounts.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from typing import Optional, Union

import asyncpg
from sftkit.database import Connection
from sftkit.error import InvalidArgument
from sftkit.service import Service, with_db_connection, with_db_transaction

from abrechnung.config import Config
from abrechnung.core.auth import check_group_permissions, create_group_log
from abrechnung.core.decorators import (
requires_group_permissions,
with_group_last_changed_update,
)
from abrechnung.core.errors import InvalidCommand, NotFoundError
from abrechnung.core.service import Service
from abrechnung.domain.accounts import (
Account,
AccountType,
Expand All @@ -18,13 +20,11 @@
)
from abrechnung.domain.groups import GroupMember
from abrechnung.domain.users import User
from abrechnung.framework.database import Connection
from abrechnung.framework.decorators import with_db_connection, with_db_transaction

from .common import _get_or_create_tag_ids


class AccountService(Service):
class AccountService(Service[Config]):
@staticmethod
async def _commit_revision(conn: asyncpg.Connection, revision_id: int):
"""Touches a revision to have all associated constraints run"""
Expand Down Expand Up @@ -57,15 +57,17 @@ async def _check_account_permissions(
account_id,
)
if not result:
raise NotFoundError(f"account not found")
raise InvalidArgument(f"account not found")

if can_write and not (result["can_write"] or result["is_owner"]):
raise PermissionError(f"user does not have write permissions")

if account_type:
type_check = [account_type] if isinstance(account_type, str) else account_type
if result["type"] not in type_check:
raise InvalidCommand(f"Transaction type {result['type']} does not match the expected type {type_check}")
raise InvalidArgument(
f"Transaction type {result['type']} does not match the expected type {type_check}"
)

return result["group_id"], result["type"]

Expand All @@ -91,7 +93,7 @@ async def _get_or_create_pending_account_change(self, conn: asyncpg.Connection,
)

if last_committed_revision is None:
raise InvalidCommand(f"Cannot edit account {account_id} as it has no committed changes.")
raise InvalidArgument(f"Cannot edit account {account_id} as it has no committed changes.")

# copy all existing transaction data into a new history entry
await conn.execute(
Expand Down Expand Up @@ -124,7 +126,7 @@ async def _check_account_exists(conn: asyncpg.Connection, group_id: int, account
account_id,
)
if not acc:
raise NotFoundError(f"Account with id {account_id}")
raise InvalidArgument(f"Account with id {account_id}")

return True

Expand Down Expand Up @@ -210,7 +212,7 @@ async def create_account(
group_membership: GroupMember,
) -> int:
if account.clearing_shares and account.type != AccountType.clearing:
raise InvalidCommand(
raise InvalidArgument(
f"'{account.type.value}' accounts cannot have associated settlement distribution shares"
)

Expand Down Expand Up @@ -279,7 +281,7 @@ async def update_account(
)
membership = await check_group_permissions(conn=conn, group_id=group_id, user=user, can_write=True)
if account.clearing_shares and account_type != AccountType.clearing.value:
raise InvalidCommand(f"'{account_type}' accounts cannot have associated settlement distribution shares")
raise InvalidArgument(f"'{account_type}' accounts cannot have associated settlement distribution shares")

revision_id = await self._create_revision(conn=conn, user=user, account_id=account_id)

Expand Down Expand Up @@ -350,7 +352,7 @@ async def delete_account(
account_id,
)
if row is None:
raise InvalidCommand(f"Account does not exist")
raise InvalidArgument(f"Account does not exist")

# TODO: FIXME move this check into the database

Expand All @@ -374,28 +376,28 @@ async def delete_account(
)

if has_shares or has_usages:
raise InvalidCommand(f"Cannot delete an account that is references by a transaction")
raise InvalidArgument(f"Cannot delete an account that is references by a transaction")

if has_clearing_shares:
raise InvalidCommand(f"Cannot delete an account that is references by a clearing account")
raise InvalidArgument(f"Cannot delete an account that is references by a clearing account")

row = await conn.fetchrow(
"select name, revision_id, deleted from account_state_valid_at() where account_id = $1",
account_id,
)
if row is None:
raise InvalidCommand(f"Cannot delete an account without any committed changes")
raise InvalidArgument(f"Cannot delete an account without any committed changes")

if row["deleted"]:
raise InvalidCommand(f"Cannot delete an already deleted account")
raise InvalidArgument(f"Cannot delete an already deleted account")

has_clearing_shares = await conn.fetchval(
"select exists (select from account_state_valid_at() p where not p.deleted and $1 = any(p.involved_accounts))",
account_id,
)

if has_clearing_shares:
raise InvalidCommand(f"Cannot delete an account that is references by another clearing account")
raise InvalidArgument(f"Cannot delete an account that is references by another clearing account")

revision_id = await conn.fetchval(
"insert into account_revision (user_id, account_id) values ($1, $2) returning id",
Expand Down
4 changes: 2 additions & 2 deletions abrechnung/application/common.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import Optional

import asyncpg
from sftkit.database import Connection


async def _get_or_create_tag_ids(*, conn: asyncpg.Connection, group_id: int, tags: Optional[list[str]]) -> list[int]:
async def _get_or_create_tag_ids(*, conn: Connection, group_id: int, tags: Optional[list[str]]) -> list[int]:
if not tags or len(tags) <= 0:
return []
tag_rows = await conn.fetch(
Expand Down
20 changes: 10 additions & 10 deletions abrechnung/application/groups.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from datetime import datetime

import asyncpg
from sftkit.database import Connection
from sftkit.error import InvalidArgument
from sftkit.service import Service, with_db_transaction

from abrechnung.config import Config
from abrechnung.core.auth import create_group_log
from abrechnung.core.decorators import (
requires_group_permissions,
with_group_last_changed_update,
)
from abrechnung.core.errors import InvalidCommand, NotFoundError
from abrechnung.core.service import Service
from abrechnung.domain.accounts import AccountType
from abrechnung.domain.groups import (
Group,
Expand All @@ -18,11 +20,9 @@
GroupPreview,
)
from abrechnung.domain.users import User
from abrechnung.framework.database import Connection
from abrechnung.framework.decorators import with_db_transaction


class GroupService(Service):
class GroupService(Service[Config]):
@with_db_transaction
async def create_group(
self,
Expand Down Expand Up @@ -116,7 +116,7 @@ async def delete_invite(
group_id,
)
if not deleted_id:
raise NotFoundError(f"No invite with the given id exists")
raise InvalidArgument(f"No invite with the given id exists")
await create_group_log(conn=conn, group_id=group_id, user=user, type="invite-deleted")

async def _create_user_account(self, conn: asyncpg.Connection, group_id: int, user: User) -> int:
Expand Down Expand Up @@ -165,7 +165,7 @@ async def join_group(self, *, conn: Connection, user: User, invite_token: str) -
invite["group_id"],
)
if user_is_already_member:
raise InvalidCommand(f"User is already a member of this group")
raise InvalidArgument(f"User is already a member of this group")

await conn.execute(
"insert into group_membership (user_id, group_id, invited_by, can_write, is_owner) "
Expand Down Expand Up @@ -249,7 +249,7 @@ async def update_member_permissions(
is_owner: bool,
):
if user.id == member_id:
raise InvalidCommand(f"group members cannot modify their own privileges")
raise InvalidArgument(f"group members cannot modify their own privileges")

# not possible to have an owner without can_write
can_write = can_write if not is_owner else True
Expand All @@ -260,7 +260,7 @@ async def update_member_permissions(
member_id,
)
if membership is None:
raise NotFoundError(f"member with id {member_id} does not exist")
raise InvalidArgument(f"member with id {member_id} does not exist")

if membership["is_owner"] == is_owner and membership["can_write"] == can_write: # no changes
return
Expand Down Expand Up @@ -330,7 +330,7 @@ async def delete_group(self, *, conn: Connection, user: User, group_id: int):
group_id,
)
if n_members != 1:
raise PermissionError(f"Can only delete a group when you are the last member")
raise InvalidArgument(f"Can only delete a group when you are the last member")

await conn.execute("delete from grp where id = $1", group_id)

Expand Down
Loading

0 comments on commit fcddfe1

Please sign in to comment.