Skip to content

Commit 6aea789

Browse files
committed
Merge branch 'main' into oidc
2 parents af5ba97 + 62a1365 commit 6aea789

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1231
-535
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
make ci-test
2727
- name: Stop and remove containers
2828
run: |
29-
make down
29+
make ci-down
3030
3131
docker:
3232
name: Docker Build

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ __pycache__
33
*.db
44
.pytest_cache
55
.venv
6-
secrets.env
6+
secrets.prod.env
7+
secrets.dev.env

.pre-commit-config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
repos:
2+
- repo: https://github.com/PyCQA/isort
3+
rev: 6.0.1
4+
hooks:
5+
- id: isort
6+
args: ["--profile=black"]
7+
- repo: https://github.com/psf/black
8+
rev: 25.1.0
9+
hooks:
10+
- id: black
11+
language_version: python3.12

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ ci-daemon:
3030
ci-test:
3131
docker compose -f $(COMPOSE_BASE) -f $(CI_COMPOSE) exec api pytest -v -s --log-level DEBUG
3232

33+
ci-down:
34+
docker compose -f $(COMPOSE_BASE) -f $(CI_COMPOSE) down
35+
3336
# Stops all running services
3437
down:
35-
docker compose down
38+
docker compose -f $(COMPOSE_BASE) -f $(DEV_COMPOSE) -f $(PROD_COMPOSE) down
3639

3740
test: dev-daemon
3841
docker compose -f $(COMPOSE_BASE) -f $(DEV_COMPOSE) exec api pytest

README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ uv remove packagename
7373
uv sync
7474
```
7575

76+
install pre-commit hook
77+
```
78+
source ./.venv/bin/activate
79+
pre-commit install
80+
```
81+
7682
### tests
7783
```
7884
make test
@@ -108,14 +114,15 @@ open http://localhost:8000/docs and http://localhost:9000
108114
- [x] add split participants by a tag
109115
- [x] ~~grafana~~, statistics
110116
- [x] treasuries
111-
- [ ] logging
112-
- [ ] permissions?
113-
- [ ] misc validation of amounts (>0.00)
114-
- [ ] improve split ux
115-
- [ ] pass tags as a list, not as add/delete operations
116-
- [ ] migrations
117+
- [x] logging
118+
- [x] postgres
117119

118120
## todo techdebt
121+
- [ ] migrations
122+
- [x] pass tags as a list, not as add/delete operations
123+
- [ ] fix ui tag management
124+
- [ ] misc validation of amounts (>0.00)
125+
- [ ] improve split ux
119126
- [ ] make a uniform deposit api CRUD, provider should be enum
120127
- [ ] update all boolean attrs to status enums
121128
- [ ] mobile ui
@@ -124,6 +131,7 @@ open http://localhost:8000/docs and http://localhost:9000
124131
- [ ] remove base service class
125132

126133
## todo future features
134+
- [ ] permissions?
127135
- [ ] deposit ui
128136
- [ ] donation categories (entities?)
129137
- [ ] easy payment urls

api/app/app.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
"""FastAPI app initialization, exception handling"""
22

3+
import json
34
import logging
45
import traceback
56

67
import uvicorn
78
from app.config import Config, get_config
89
from app.errors.base import ApplicationError
10+
from app.errors.token import TokenInvalid
911
from app.routes.balance import balance_router
1012
from app.routes.currency_exchange import currency_exchange_router
1113
from app.routes.deposit_provider_callbacks import deposit_provider_callbacks_router
1214
from app.routes.deposits import deposits_router
1315
from app.routes.entity import entity_router
16+
from app.routes.oidc import router as oidc_router
1417
from app.routes.resident_fee import router as resident_fee_router
1518
from app.routes.split import split_router
1619
from app.routes.stats import router as stats_router
1720
from app.routes.tag import tag_router
1821
from app.routes.token import token_router
1922
from app.routes.transaction import transaction_router
2023
from app.routes.treasury import treasury_router
21-
from app.routes.oidc import router as oidc_router
2224
from fastapi import FastAPI, Request
2325
from fastapi.exceptions import ResponseValidationError
2426
from fastapi.middleware.cors import CORSMiddleware
@@ -42,7 +44,9 @@
4244
secret_key=config.secret_key,
4345
)
4446
if not config.secret_key:
45-
raise ValueError("SECRET_KEY is missing in the configuration. Please set a valid secret key.")
47+
raise ValueError(
48+
"SECRET_KEY is missing in the configuration. Please set a valid secret key."
49+
)
4650

4751

4852
@app.exception_handler(ResponseValidationError)
@@ -60,24 +64,31 @@ async def response_validation_exception_handler(
6064

6165
@app.exception_handler(ApplicationError)
6266
def application_exception_handler(request: Request, exc: ApplicationError):
63-
traceback.print_exception(exc)
67+
c = {
68+
"error_code": exc.error_code,
69+
"error": exc.error,
70+
"where": exc.where,
71+
}
72+
logger.error(c)
73+
# Only print full traceback when in debug logging
74+
if logger.isEnabledFor(logging.DEBUG):
75+
traceback.print_exception(exc)
6476
e = JSONResponse(
6577
status_code=exc.http_code or 418,
66-
content={
67-
"error_code": exc.error_code,
68-
"error": exc.error,
69-
"where": exc.where,
70-
},
78+
content=c,
7179
)
7280
return e
7381

7482

7583
@app.exception_handler(SQLAlchemyError)
7684
def sqlite_exception_handler(request: Request, exc: SQLAlchemyError):
77-
traceback.print_exception(exc)
85+
logger.error(exc)
86+
# Only print full traceback when in debug logging
87+
if logger.isEnabledFor(logging.DEBUG):
88+
traceback.print_exception(exc)
7889
e = JSONResponse(
7990
status_code=418,
80-
content={"error_code": 4000, "error": exc._message()},
91+
content={"error_code": 1500, "error": exc._message()},
8192
)
8293
return e
8394

@@ -97,4 +108,4 @@ def sqlite_exception_handler(request: Request, exc: SQLAlchemyError):
97108
app.include_router(oidc_router)
98109

99110
if __name__ == "__main__":
100-
uvicorn.run(app, host="0.0.0.0", port=8000)
111+
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")

api/app/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class Config:
2424
cryptapi_address_trc20_usdt: str | None = field(
2525
default=getenv("REFINANCE_CRYPTAPI_ADDRESS_TRC20_USDT", "")
2626
)
27+
# Optional database URL for Postgres or other databases
28+
database_url_env: str | None = field(default=getenv("REFINANCE_DATABASE_URL", None))
2729

2830
# OIDC configuration
2931
oidc_client_id: str | None = field(default=getenv("REFINANCE_OIDC_CLIENT_ID", ""))
@@ -43,6 +45,9 @@ def database_path(self) -> Path:
4345

4446
@property
4547
def database_url(self) -> str:
48+
# Use provided DATABASE_URL if available, else fall back to SQLite file
49+
if self.database_url_env:
50+
return self.database_url_env
4651
return f"sqlite:///{self.database_path}"
4752

4853

api/app/db.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from contextlib import contextmanager
66
from typing import Any, Generator, List, Type
77

8-
from app.bootstrap import BOOTSTRAP
98
from app.config import Config, get_config
109
from app.models.base import BaseModel
10+
from app.seeding import SEEDING
1111
from fastapi import Depends
1212
from sqlalchemy import Engine, create_engine, text
1313
from sqlalchemy.exc import SQLAlchemyError
@@ -25,9 +25,14 @@ class DatabaseConnection:
2525
def __init__(self, config: Config = Depends(get_config)) -> None:
2626
# Ensure the database folder exists.
2727
os.makedirs(config.database_path.parent, exist_ok=True)
28-
self.engine = create_engine(
29-
config.database_url, connect_args={"check_same_thread": False}
30-
)
28+
# Create engine with appropriate connection args for SQLite or other databases
29+
db_url = config.database_url
30+
if db_url.startswith("sqlite"): # SQLite needs check_same_thread
31+
self.engine = create_engine(
32+
db_url, connect_args={"check_same_thread": False}
33+
)
34+
else:
35+
self.engine = create_engine(db_url)
3136
self.session_local = sessionmaker(
3237
autocommit=False,
3338
autoflush=False,
@@ -62,7 +67,7 @@ def seed_bootstrap_data(self) -> None:
6267
This method is called once during initialization.
6368
"""
6469
with self.get_session() as session:
65-
for model, seeds in BOOTSTRAP.items():
70+
for model, seeds in SEEDING.items():
6671
try:
6772
self._seed_model(
6873
session=session,
@@ -104,7 +109,6 @@ def _seed_model(
104109
logger.info("Seeding data for table '%s'", table_name)
105110

106111
for seed in seeds:
107-
logger.debug("Merging seed with id %s for table '%s'", seed.id, table_name)
108112
session.merge(seed)
109113
session.flush()
110114
logger.info("Merged %d seed(s) for table '%s'", len(seeds), table_name)
@@ -130,7 +134,24 @@ def _seed_model(
130134
logger.info(
131135
"Current sequence for table '%s': %d", table_name, current_seq
132136
)
133-
if current_seq < (sequence_start - 1):
137+
# Determine the current max ID in the table
138+
max_id_result = session.execute(
139+
text(f"SELECT MAX(id) FROM {table_name}")
140+
).fetchone()
141+
max_id = (
142+
max_id_result[0]
143+
if max_id_result and max_id_result[0] is not None
144+
else 0
145+
)
146+
logger.info("Max id for table '%s': %d", table_name, max_id)
147+
if max_id >= sequence_start:
148+
logger.info(
149+
"Skipping sequence update for table '%s' as max id %d >= desired start %d",
150+
table_name,
151+
max_id,
152+
sequence_start,
153+
)
154+
elif current_seq < (sequence_start - 1):
134155
logger.info(
135156
"Updating sequence for table '%s' to %d",
136157
table_name,
@@ -161,7 +182,24 @@ def _seed_model(
161182
sequence_name,
162183
current_seq,
163184
)
164-
if current_seq < (sequence_start - 1):
185+
# Determine the current max ID in the table
186+
max_id_result = session.execute(
187+
text(f"SELECT MAX(id) FROM {table_name}")
188+
).fetchone()
189+
max_id = (
190+
max_id_result[0]
191+
if max_id_result and max_id_result[0] is not None
192+
else 0
193+
)
194+
logger.info("Max id for table '%s': %d", table_name, max_id)
195+
if max_id >= sequence_start:
196+
logger.info(
197+
"Skipping sequence update for table '%s' as max id %d >= desired start %d",
198+
table_name,
199+
max_id,
200+
sequence_start,
201+
)
202+
elif current_seq < (sequence_start - 1):
165203
logger.info(
166204
"Updating sequence for table '%s' (sequence: '%s') to %d",
167205
table_name,

api/app/models/tag.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,3 @@ class Tag(BaseModel):
88
__tablename__ = "tags"
99

1010
name: Mapped[str] = mapped_column(unique=True)
11-
12-
13-
# this list is used by db.py to create system tags
14-
TAG_BOOTSTRAP: list[Tag] = [
15-
Tag(id=1, name="sys", comment="things defined in refinance code logic"),
16-
Tag(id=2, name="resident", comment="hackerspace residents"),
17-
Tag(id=3, name="fee", comment="monthly resident's fee"),
18-
]

api/app/routes/entity.py

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
EntitySchema,
1010
EntityUpdateSchema,
1111
)
12-
from app.schemas.tag import TagSchema
1312
from app.services.entity import EntityService
1413
from fastapi import APIRouter, Depends
1514

@@ -60,23 +59,3 @@ def update_entity(
6059
actor_entity: Entity = Depends(get_entity_from_token),
6160
):
6261
return entity_service.update(entity_id, entity_update)
63-
64-
65-
@entity_router.post("/{entity_id}/tags", response_model=TagSchema)
66-
def add_tag_to_entity(
67-
entity_id: int,
68-
tag_id: int,
69-
entity_service: EntityService = Depends(),
70-
actor_entity: Entity = Depends(get_entity_from_token),
71-
):
72-
return entity_service.add_tag(entity_id, tag_id)
73-
74-
75-
@entity_router.delete("/{entity_id}/tags", response_model=TagSchema)
76-
def remove_tag_from_entity(
77-
entity_id: int,
78-
tag_id: int,
79-
entity_service: EntityService = Depends(),
80-
actor_entity: Entity = Depends(get_entity_from_token),
81-
):
82-
return entity_service.remove_tag(entity_id, tag_id)

0 commit comments

Comments
 (0)