Skip to content

Commit

Permalink
Merge pull request #136 from EduardSchwarzkopf/add-scheduled-transact…
Browse files Browse the repository at this point in the history
…ions

feat: Add scheduled transactions
  • Loading branch information
EduardSchwarzkopf authored Aug 11, 2024
2 parents ba13357 + 4f816e7 commit b657d52
Show file tree
Hide file tree
Showing 65 changed files with 3,608 additions and 865 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,5 @@ logs/
.env.*
!.env.test

start-dev.sh
start-dev.sh
docker-compose.db.yml
14 changes: 7 additions & 7 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
"args": [
"--app",
"app.celery",
"worker",
"--loglevel",
"DEBUG",
"-b",
"redis://redis:6379/0",
"--broker",
"redis://127.0.0.1:6379/0",
"--result-backend",
"redis://redis:6379/0",
"redis://127.0.0.1:6379/0",
"worker",
"--pool",
"solo"
"solo",
"--loglevel",
"DEBUG"
]
},
{
Expand Down
4 changes: 2 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
{
"label": "start-docker-compose",
"type": "shell",
"command": "docker compose -f ${workspaceFolder}/docker-compose.dev.yml up -d"
"command": "docker compose --env-file ${workspaceFolder}/.env.dev -f ${workspaceFolder}/docker-compose.dev.yml up -d"
},
{
"label": "stop-docker-compose",
"type": "shell",
"command": "docker compose -f ${workspaceFolder}/docker-compose.dev.yml down"
"command": "docker compose --env-file ${workspaceFolder}/.env.dev -f ${workspaceFolder}/docker-compose.dev.yml down"
},
{
"label": "start-test-suite",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""add is_active to scheduled transactions
Revision ID: 3d95cdfbdd44
Revises: b8097c295e4a
Create Date: 2024-06-23 09:14:17.818896
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '3d95cdfbdd44'
down_revision: Union[str, None] = 'b8097c295e4a'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('transactions_scheduled', sa.Column('is_active', sa.Boolean(), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('transactions_scheduled', 'is_active')
# ### end Alembic commands ###
8 changes: 5 additions & 3 deletions alembic/versions/4885a032ce2e_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
Create Date: 2023-05-09 12:49:37.994900
"""
from alembic import op

import fastapi_users_db_sqlalchemy
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
import fastapi_users_db_sqlalchemy

from alembic import op
from app import models
from app.data import categories
from app.data.frequencies import get_frequency_list
from app import models

# revision identifiers, used by Alembic.
revision = "4885a032ce2e"
Expand Down
46 changes: 46 additions & 0 deletions alembic/versions/b8097c295e4a_update_transaction_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""update transaction model
Revision ID: b8097c295e4a
Revises: ebc9a4757806
Create Date: 2024-05-20 17:41:41.567122
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'b8097c295e4a'
down_revision: Union[str, None] = 'ebc9a4757806'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('transactions', sa.Column('scheduled_transaction_id', sa.Integer(), nullable=True))
op.alter_column('transactions', 'account_id',
existing_type=sa.INTEGER(),
nullable=False)
op.alter_column('transactions', 'information_id',
existing_type=sa.INTEGER(),
nullable=False)
op.create_unique_constraint('uq_scheduled_transaction_date', 'transactions', ['scheduled_transaction_id', 'information_id'])
op.create_foreign_key(None, 'transactions', 'transactions_scheduled', ['scheduled_transaction_id'], ['id'], ondelete='SET NULL')
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'transactions', type_='foreignkey')
op.drop_constraint('uq_scheduled_transaction_date', 'transactions', type_='unique')
op.alter_column('transactions', 'information_id',
existing_type=sa.INTEGER(),
nullable=True)
op.alter_column('transactions', 'account_id',
existing_type=sa.INTEGER(),
nullable=True)
op.drop_column('transactions', 'scheduled_transaction_id')
# ### end Alembic commands ###
30 changes: 30 additions & 0 deletions alembic/versions/dbce8a11ecc2_add_default_value_for_is_active.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""add default value for is_active
Revision ID: dbce8a11ecc2
Revises: 3d95cdfbdd44
Create Date: 2024-06-23 09:21:50.927292
"""

from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "dbce8a11ecc2"
down_revision: Union[str, None] = "3d95cdfbdd44"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade():
op.alter_column(
"transactions_scheduled", "is_active", server_default=sa.text("true")
)
op.execute("UPDATE transactions_scheduled SET is_active = true")


def downgrade():
op.alter_column("transactions_scheduled", "is_active", server_default=None)
2 changes: 2 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class Settings(BaseSettings):
celery_broker_url: str = "redis://127.0.0.1:6379/0"
celery_result_backend: str = "redis://127.0.0.1:6379/0"

batch_size: int = 1000

def __init__(self, **values):
super().__init__(**values)
self.configure_settings()
Expand Down
10 changes: 7 additions & 3 deletions app/data/frequencies.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from app.utils.enums import Frequency


def get_frequency_list():
"""
Returns a list of all available frequencies.
Expand All @@ -6,6 +9,7 @@ def get_frequency_list():
Each dictionary contains the id and label of a frequency.
"""

frequencies = ["once", "daily", "weekly", "monthly", "yearly"]

return [{"id": i + 1, "label": frequencies[i]} for i in range(len(frequencies))]
return [
{"id": frequency.value, "label": str(frequency.name).lower()}
for frequency in Frequency.get_list()
]
82 changes: 77 additions & 5 deletions app/date_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from contextlib import suppress
from datetime import datetime as dt
from datetime import timedelta, timezone
from typing import Optional


def today():
"""Get the current UTC date with time set to 00:00:00.
def get_today():
"""
Get the current UTC date with time set to 00:00:00.
Args:
None
Expand All @@ -19,8 +21,66 @@ def today():
return dt.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)


def now():
"""
Get the current date and time in UTC timezone.
"""

return dt.now(timezone.utc)


def get_day_delta(date: dt, days: int) -> dt:
"""
Get the date by adding a specified number of days to the given date.
Args:
date: The reference date.
days: The number of days to add to the reference date.
Returns:
The resulting date after adding the specified number of days.
"""

return date + timedelta(days=days)


def get_tomorrow(date: Optional[dt] = None) -> dt:
"""
Get the date for tomorrow.
Args:
date: The reference date. Defaults to today if not provided.
Returns:
The date for tomorrow.
"""

if date is None:
date = get_today()

return get_day_delta(date, 1)


def get_yesterday(date: Optional[dt] = None) -> dt:
"""
Get the date for tomorrow.
Args:
date: The reference date. Defaults to today if not provided.
Returns:
The date for tomorrow.
"""

if date is None:
date = get_today()

return get_day_delta(date, -1)


def get_datetime_from_timestamp(timestamp):
"""Convert a timestamp to a datetime object.
"""
Convert a timestamp to a datetime object.
Args:
timestamp: The timestamp to convert.
Expand All @@ -43,7 +103,8 @@ def get_datetime_from_timestamp(timestamp):


def string_to_datetime(str_date: str):
"""Convert a string date to a datetime object.
"""
Convert a string date to a datetime object.
Args:
str_date: The string date to convert.
Expand Down Expand Up @@ -104,7 +165,8 @@ def _extracted_from_string_to_datetime(str_date: str):


def get_last_day_of_month(date: dt):
"""Returns last day of month
"""
Returns last day of month
Args:
dt (datetime): date to get the last day of month
Expand All @@ -120,3 +182,13 @@ def get_last_day_of_month(date: dt):
last_day_of_month = next_month - timedelta(days=next_month.day)

return last_day_of_month.day


def get_iso_timestring() -> str:
"""
Returns the current time in ISO 8601 format.
Returns:
str: The current time in ISO 8601 format.
"""
return datetime.datetime.now().isoformat()
6 changes: 6 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import arel
import jwt
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from fastapi import FastAPI, Request, Response, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
Expand All @@ -25,10 +26,12 @@
from app.middleware import HeaderLinkMiddleware
from app.repository import Repository
from app.routes import router_list
from app.scheduled_tasks import add_jobs_to_scheduler
from app.utils import BreadcrumbBuilder
from app.utils.exceptions import UnauthorizedPageException

logger = get_logger(__name__)
scheduler = AsyncIOScheduler()


@asynccontextmanager
Expand All @@ -46,14 +49,17 @@ async def lifespan(_api_app: FastAPI):

try:
await db.init()
add_jobs_to_scheduler(scheduler)
yield
finally:
scheduler.shutdown()
if db.session is not None:
await db.session.close()


app = FastAPI(lifespan=lifespan)


app.mount("/static", StaticFiles(directory="static"), name="static")

origins = ["http://127.0.0.1:5173", "http://127.0.0.1"]
Expand Down
Loading

0 comments on commit b657d52

Please sign in to comment.