Skip to content

Commit

Permalink
Merge pull request #3 from f0rthsp4ce/crow-fff
Browse files Browse the repository at this point in the history
add balance cache
  • Loading branch information
MikeWent authored Jul 23, 2024
2 parents a2b6462 + 9a06cb3 commit 6598955
Show file tree
Hide file tree
Showing 36 changed files with 373 additions and 403 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
*.pyc
__pycache__/
*.db
secrets.env
255 changes: 57 additions & 198 deletions Pipfile.lock

Large diffs are not rendered by default.

30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,21 @@ sum of all transactions. both confirmed and not. separated.
mark entities and transactions for quick search.

### security
`X-Token` header is used for authentication.

token will be sent to `telegram_id` of an entity. newly generated tokens do not revoke old ones.
- `X-Token` header is used for authentication.
- you may request a new token any time: `POST /tokens/send` with `name`, `id` or `telegram_id` of your entity — anything you remember.
- token (link) will be sent to `telegram_id` of the entity. newly generated tokens do NOT revoke old ones.

you may request a new token any time: with `name`, `id` or `telegram_id` of your entity — anything you remember.

> token — [jwt](http://jwt.io) with entity id & timestamp inside. basically it's a server-signed & verifiable json, base64'd.
> token — [jwt](http://jwt.io) string with entity id & timestamp inside. basically `base64(sign(json(id=123, date=now())))`.
## run
```console
docker compose up
```
API: http://localhost:8000/docs
UI: http://localhost:5000


## develop
### development
run backend & frontend with live code reload:
```
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
```
open http://localhost:8000/docs and http://localhost:5000
open http://localhost:8000/docs and http://localhost:9000

create local environment with all dependencies:
```console
Expand All @@ -65,6 +58,15 @@ cd api
pytest
```

### production
put secrets into `secrets.env`. see `secrets.env.example` as a reference.

```console
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```
API: http://0.0.0.0:8000/docs
UI: http://0.0.0.0:9000


## todo
- [x] base classes
Expand All @@ -75,7 +77,7 @@ pytest
- [x] tags
- [x] transactions
- [x] balances
- [ ] balance cache
- [x] balance cache
- [x] date range search
- [ ] recurrent payments
- [ ] donation categories
Expand Down
9 changes: 5 additions & 4 deletions api/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

import logging

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from sqlalchemy.exc import SQLAlchemyError

from app.config import Config, get_config
from app.errors.base import ApplicationError
from app.routes.balance import balance_router
from app.routes.entity import entity_router
from app.routes.tag import tag_router
from app.routes.token import token_router
from app.routes.transaction import transaction_router
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from sqlalchemy.exc import SQLAlchemyError

logger = logging.getLogger(__name__)

Expand Down
5 changes: 3 additions & 2 deletions api/app/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import os

from app.config import Config, get_config
from app.models.base import BaseModel
from fastapi import Depends
from sqlalchemy import Engine, create_engine
from sqlalchemy.orm import Session, sessionmaker

from app.config import Config, get_config
from app.models.base import BaseModel


class DatabaseConnection:
engine: Engine
Expand Down
3 changes: 2 additions & 1 deletion api/app/middlewares/token.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Middleware for Entity authentication"""

from app.services.token import TokenService
from fastapi import Depends, Header

from app.services.token import TokenService


def get_entity_from_token(
x_token: str = Header(
Expand Down
5 changes: 3 additions & 2 deletions api/app/models/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

from typing import List

from app.models.base import BaseModel
from app.models.tag import Tag
from sqlalchemy import Column, ForeignKey, Table
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.models.base import BaseModel
from app.models.tag import Tag

entities_tags = Table(
"entities_tags",
BaseModel.metadata,
Expand Down
3 changes: 2 additions & 1 deletion api/app/models/tag.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Tag model"""

from app.models.base import BaseModel
from sqlalchemy.orm import Mapped, mapped_column

from app.models.base import BaseModel


class Tag(BaseModel):
__tablename__ = "tags"
Expand Down
5 changes: 3 additions & 2 deletions api/app/models/transaction.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Transaction model"""

from sqlalchemy import DECIMAL, Column, ForeignKey, String, Table
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.models.base import BaseModel
from app.models.entity import Entity
from app.models.tag import Tag
from sqlalchemy import DECIMAL, Column, ForeignKey, String, Table
from sqlalchemy.orm import Mapped, mapped_column, relationship

transactions_tags = Table(
"transactions_tags",
Expand Down
9 changes: 3 additions & 6 deletions api/app/routes/balance.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
"""API routes for Balance observing"""

from datetime import datetime

from fastapi import APIRouter, Depends

from app.middlewares.token import get_entity_from_token
from app.models.entity import Entity
from app.schemas.balance import BalanceSchema
from app.services.balance import BalanceService
from fastapi import APIRouter, Depends

balance_router = APIRouter(prefix="/balances", tags=["Balances"])


@balance_router.get("/{entity_id}", response_model=BalanceSchema)
def get_balance(
entity_id: int,
specific_date: datetime | None = None,
balance_service: BalanceService = Depends(),
actor_entity: Entity = Depends(get_entity_from_token),
):
return balance_service.get_balances(
entity_id=entity_id, specific_date=specific_date
)
return balance_service.get_balances(entity_id=entity_id)
3 changes: 2 additions & 1 deletion api/app/routes/entity.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""API routes for Entity manipulation"""

from fastapi import APIRouter, Depends

from app.errors.entity import EntitiesAlreadyPresent
from app.middlewares.token import get_entity_from_token
from app.models.entity import Entity
Expand All @@ -12,7 +14,6 @@
)
from app.schemas.tag import TagSchema
from app.services.entity import EntityService
from fastapi import APIRouter, Depends

entity_router = APIRouter(prefix="/entities", tags=["Entities"])

Expand Down
3 changes: 2 additions & 1 deletion api/app/routes/tag.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""API routes for Tag manipulation"""

from fastapi import APIRouter, Depends

from app.middlewares.token import get_entity_from_token
from app.models.entity import Entity
from app.schemas.base import PaginationSchema
Expand All @@ -10,7 +12,6 @@
TagUpdateSchema,
)
from app.services.tag import TagService
from fastapi import APIRouter, Depends

tag_router = APIRouter(prefix="/tags", tags=["Tags"])

Expand Down
3 changes: 2 additions & 1 deletion api/app/routes/token.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""API routes for Token manipulation"""

from fastapi import APIRouter, Depends

from app.schemas.token import TokenSendReportSchema
from app.services.token import TokenService
from fastapi import APIRouter, Depends

token_router = APIRouter(prefix="/tokens", tags=["Tokens"])

Expand Down
3 changes: 2 additions & 1 deletion api/app/routes/transaction.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""API routes for Transaction manipulation"""

from fastapi import APIRouter, Depends

from app.middlewares.token import get_entity_from_token
from app.models.entity import Entity
from app.schemas.base import PaginationSchema
Expand All @@ -11,7 +13,6 @@
TransactionUpdateSchema,
)
from app.services.transaction import TransactionService
from fastapi import APIRouter, Depends

transaction_router = APIRouter(prefix="/transactions", tags=["Transactions"])

Expand Down
3 changes: 2 additions & 1 deletion api/app/schemas/mixins/tags_filter_mixin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Mixin of a schema of a model which supports item tagging"""

from app.schemas.base import BaseFilterSchema
from fastapi import Query
from pydantic import Field

from app.schemas.base import BaseFilterSchema


class TagsFilterSchemaMixin(BaseFilterSchema):
tags_ids: list[int] = Field(Query([]))
3 changes: 2 additions & 1 deletion api/app/schemas/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

from decimal import Decimal

from pydantic import field_validator, model_validator

from app.schemas.base import BaseFilterSchema, BaseReadSchema, BaseUpdateSchema
from app.schemas.entity import EntitySchema
from app.schemas.mixins.tags_filter_mixin import TagsFilterSchemaMixin
from app.schemas.tag import TagSchema
from pydantic import field_validator, model_validator


class TransactionSchema(BaseReadSchema):
Expand Down
48 changes: 28 additions & 20 deletions api/app/services/balance.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
"""Balance service"""

from datetime import datetime
from decimal import Decimal

from fastapi import Depends
from sqlalchemy.orm import Session
from sqlalchemy.sql import func, select

from app.db import get_db
from app.models.transaction import Transaction
from app.schemas.balance import BalanceSchema
from app.services.entity import EntityService
from fastapi import Depends
from sqlalchemy.orm import Session
from sqlalchemy.sql import func, select


class BalanceService:
_cache = {}

def __init__(
self,
db: Session = Depends(get_db),
Expand All @@ -21,17 +23,31 @@ def __init__(
self.db = db
self.entity_service = entity_service

def get_balances(
self, entity_id: int, specific_date: datetime | None = None
) -> BalanceSchema:
def invalidate_cache_entry(self, entity_id: int):
self._cache.pop(entity_id, None)

def get_balances(self, entity_id: int) -> BalanceSchema:
"""
Calculates the current balances for a given entity across all currencies.
Only confirmed transactions are considered.
Calculate momentary balances for a given entity across all currencies.
- confirmed and non-confirmed transactions are counted separately.
- currencies are counted separately.
Internal in-RAM cache stores balances of each entity.
Balance cache for a particular entity is invalidated when transaction from/to
entity is created, edited (confirmed) or deleted.
"""
if entity_id in self._cache:
return self._cache[entity_id]
else:
result = self._get_balances(entity_id)
self._cache[entity_id] = result
return result

def _get_balances(self, entity_id: int) -> BalanceSchema:
# Check that entity exists
self.entity_service.get(entity_id)

# Function to process transactions based on confirmation status
# Function to sum transactions based on confirmation status
def sum_transactions(confirmed: bool) -> dict[str, Decimal]:
# Query to get sum of all incoming transactions
credit_query = select(
Expand All @@ -51,15 +67,6 @@ def sum_transactions(confirmed: bool) -> dict[str, Decimal]:
Transaction.confirmed == confirmed,
)

# Date limiter — don't count transactions after this date
if specific_date is not None:
credit_query = credit_query.filter(
Transaction.created_at <= specific_date
)
debit_query = debit_query.filter(
Transaction.created_at <= specific_date
)

# Group sums by currency
credit_query = credit_query.group_by(Transaction.currency)
debit_query = debit_query.group_by(Transaction.currency)
Expand All @@ -86,7 +93,8 @@ def sum_transactions(confirmed: bool) -> dict[str, Decimal]:

return total_by_currency

return BalanceSchema(
result = BalanceSchema(
confirmed=sum_transactions(confirmed=True),
non_confirmed=sum_transactions(confirmed=False),
)
return result
Loading

0 comments on commit 6598955

Please sign in to comment.