Spaces:
Sleeping
Sleeping
users, transactions crud apis
#4
by
praneethys
- opened
- .dockerignore +1 -3
- .env.example +0 -8
- .gitignore +1 -2
- .vscode/settings.json +1 -9
- Dockerfile +0 -9
- Dockerfile.local.postgres +0 -23
- Makefile +0 -14
- README.md +0 -30
- alembic.ini +0 -116
- app/api/routers/chat.py +10 -5
- app/api/routers/file_upload.py +0 -43
- app/api/routers/income_statement.py +0 -64
- app/api/routers/transaction.py +0 -30
- app/api/routers/user.py +0 -89
- app/categorization/file_processing.py +28 -36
- app/engine/postgresdb.py +0 -81
- app/model/base.py +0 -15
- app/model/income_statement.py +0 -46
- app/model/transaction.py +0 -62
- app/model/user.py +0 -110
- app/schema/base.py +0 -14
- app/schema/index.py +0 -85
- app/service/income_statement.py +0 -37
- app/service/llm.py +0 -55
- app/transactions_rag/categorize_transactions.ipynb +3 -2
- config/index.py +0 -34
- data/.gitkeep +0 -0
- data/tx_data/input/transactions_2024.csv +0 -28
- main.py +30 -53
- migration/README +0 -1
- migration/env.py +0 -91
- migration/script.py.mako +0 -26
- migration/versions/4e76691ab103_add_income_statement_table.py +0 -43
- migration/versions/7ea44cbc5b1f_default_timezone.py +0 -69
- migration/versions/8feaedca36f9_users_datatype_updates.py +0 -74
- migration/versions/cd515c44401d_add_users_transactions_tables.py +0 -55
- poetry.lock +1 -398
- pyproject.toml +1 -20
- tests/conftest.py +0 -83
- tests/pytest.ini +0 -4
- tests/test_income_statement.py +0 -51
- tests/test_transactions.py +0 -25
- tests/test_users.py +0 -40
- tests/utils.py +0 -88
.dockerignore
CHANGED
@@ -1,5 +1,3 @@
|
|
1 |
__pycache__
|
2 |
storage
|
3 |
-
data/*
|
4 |
-
venv
|
5 |
-
myvenv
|
|
|
1 |
__pycache__
|
2 |
storage
|
3 |
+
data/*
|
|
|
|
.env.example
CHANGED
@@ -43,11 +43,3 @@ APP_HOST=0.0.0.0
|
|
43 |
|
44 |
# The port to start the backend app.
|
45 |
APP_PORT=8000
|
46 |
-
|
47 |
-
# Postgres database configuration
|
48 |
-
POSTGRES_USER=postgres
|
49 |
-
POSTGRES_PASSWORD=postgres
|
50 |
-
POSTGRES_DB_NAME=postgres
|
51 |
-
POSTGRES_DB_HOST=localhost
|
52 |
-
POSTGRES_DB_PORT=5432
|
53 |
-
SQLALCHEMY_DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_DB_HOST}:${POSTGRES_DB_PORT}/${POSTGRES_DB_NAME}
|
|
|
43 |
|
44 |
# The port to start the backend app.
|
45 |
APP_PORT=8000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
__pycache__
|
2 |
storage
|
3 |
.env
|
4 |
-
|
5 |
-
myvenv
|
|
|
1 |
__pycache__
|
2 |
storage
|
3 |
.env
|
4 |
+
data/*
|
|
.vscode/settings.json
CHANGED
@@ -1,11 +1,3 @@
|
|
1 |
{
|
2 |
-
"jupyter.notebookFileRoot": "${workspaceFolder}"
|
3 |
-
"[python]": {
|
4 |
-
"editor.defaultFormatter": "ms-python.black-formatter",
|
5 |
-
"editor.formatOnSave": true,
|
6 |
-
},
|
7 |
-
"ms-python.black-formatter.args": [
|
8 |
-
"--line-length",
|
9 |
-
"119"
|
10 |
-
]
|
11 |
}
|
|
|
1 |
{
|
2 |
+
"jupyter.notebookFileRoot": "${workspaceFolder}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
}
|
Dockerfile
CHANGED
@@ -25,13 +25,4 @@ COPY . .
|
|
25 |
# Make port 8000 available to the world outside this container
|
26 |
EXPOSE 8000
|
27 |
|
28 |
-
ENV POSTGRES_USER codepath_project_owner
|
29 |
-
ENV POSTGRES_PASSWORD 03EdiworgCJz
|
30 |
-
ENV POSTGRES_DB_NAME codepath_project
|
31 |
-
ENV POSTGRES_DB_HOST ep-icy-cloud-a5m4mcgo.us-east-2.aws.neon.tech
|
32 |
-
ENV POSTGRES_DB_PORT 5432
|
33 |
-
|
34 |
-
# Run migrations
|
35 |
-
RUN alembic upgrade head
|
36 |
-
|
37 |
CMD ["python", "main.py"]
|
|
|
25 |
# Make port 8000 available to the world outside this container
|
26 |
EXPOSE 8000
|
27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
CMD ["python", "main.py"]
|
Dockerfile.local.postgres
DELETED
@@ -1,23 +0,0 @@
|
|
1 |
-
# Pull the official PostgreSQL 16 image
|
2 |
-
FROM postgres:16
|
3 |
-
|
4 |
-
# Create a directory for the database files
|
5 |
-
RUN mkdir -p /var/lib/postgresql/data
|
6 |
-
|
7 |
-
# Change the ownership of the data directory
|
8 |
-
RUN chown -R postgres:postgres /var/lib/postgresql/data
|
9 |
-
|
10 |
-
# Copy the configuration files into the data directory
|
11 |
-
# NOTE: the [f] suffix is to ensure that the COPY command will not fail if the files don't exist
|
12 |
-
COPY ./pg_hba.con[f] /var/lib/postgresql/data/pg_hba.conf
|
13 |
-
COPY ./postgresql.con[f] /var/lib/postgresql/data/postgresql.conf
|
14 |
-
|
15 |
-
# Include environment variables for the PostgreSQL user and password
|
16 |
-
ENV POSTGRES_USER=${POSTGRES_USER}
|
17 |
-
ENV POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
18 |
-
|
19 |
-
# Expose the default PostgreSQL port
|
20 |
-
EXPOSE 5432
|
21 |
-
|
22 |
-
# Start the PostgreSQL server
|
23 |
-
CMD ["postgres", "-c", "config_file=/var/lib/postgresql/data/postgresql.conf"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Makefile
DELETED
@@ -1,14 +0,0 @@
|
|
1 |
-
|
2 |
-
include .env
|
3 |
-
|
4 |
-
build-postgres:
|
5 |
-
docker build -t codepath_project_postgres -f Dockerfile.local.postgres .
|
6 |
-
|
7 |
-
run-postgres:
|
8 |
-
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=${POSTGRES_PASSWORD} -e POSTGRES_USER=${POSTGRES_USER} --name ${POSTGRES_DB_NAME} codepath_project_postgres
|
9 |
-
|
10 |
-
run-migrations:
|
11 |
-
alembic upgrade head
|
12 |
-
|
13 |
-
generate-migrations:
|
14 |
-
alembic revision --autogenerate -m "$(migration_title)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
@@ -69,36 +69,6 @@ The API allows CORS for all origins to simplify development. You can change this
|
|
69 |
ENVIRONMENT=prod python main.py
|
70 |
```
|
71 |
|
72 |
-
## Local Postgres database setup
|
73 |
-
|
74 |
-
To setup a local postgres database, run:
|
75 |
-
|
76 |
-
1. Build the docker image:
|
77 |
-
|
78 |
-
```bash
|
79 |
-
make build-postgres
|
80 |
-
```
|
81 |
-
|
82 |
-
2. Start the docker container:
|
83 |
-
|
84 |
-
```bash
|
85 |
-
make run-postgres
|
86 |
-
```
|
87 |
-
|
88 |
-
## Running Migrations
|
89 |
-
|
90 |
-
To generate new migrations, run:
|
91 |
-
|
92 |
-
```bash
|
93 |
-
make generate-migrations migration_title="<name_for_migration>"
|
94 |
-
```
|
95 |
-
|
96 |
-
To locally verify your changes, run:
|
97 |
-
|
98 |
-
```bash
|
99 |
-
make run-migrations
|
100 |
-
```
|
101 |
-
|
102 |
## Using Docker
|
103 |
|
104 |
1. Build an image for the FastAPI app:
|
|
|
69 |
ENVIRONMENT=prod python main.py
|
70 |
```
|
71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
## Using Docker
|
73 |
|
74 |
1. Build an image for the FastAPI app:
|
alembic.ini
DELETED
@@ -1,116 +0,0 @@
|
|
1 |
-
# A generic, single database configuration.
|
2 |
-
|
3 |
-
[alembic]
|
4 |
-
# path to migration scripts
|
5 |
-
script_location = migration
|
6 |
-
|
7 |
-
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
8 |
-
# Uncomment the line below if you want the files to be prepended with date and time
|
9 |
-
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
10 |
-
# for all available tokens
|
11 |
-
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
12 |
-
|
13 |
-
# sys.path path, will be prepended to sys.path if present.
|
14 |
-
# defaults to the current working directory.
|
15 |
-
prepend_sys_path = .
|
16 |
-
|
17 |
-
# timezone to use when rendering the date within the migration file
|
18 |
-
# as well as the filename.
|
19 |
-
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
20 |
-
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
21 |
-
# string value is passed to ZoneInfo()
|
22 |
-
# leave blank for localtime
|
23 |
-
# timezone =
|
24 |
-
|
25 |
-
# max length of characters to apply to the
|
26 |
-
# "slug" field
|
27 |
-
# truncate_slug_length = 40
|
28 |
-
|
29 |
-
# set to 'true' to run the environment during
|
30 |
-
# the 'revision' command, regardless of autogenerate
|
31 |
-
# revision_environment = false
|
32 |
-
|
33 |
-
# set to 'true' to allow .pyc and .pyo files without
|
34 |
-
# a source .py file to be detected as revisions in the
|
35 |
-
# versions/ directory
|
36 |
-
# sourceless = false
|
37 |
-
|
38 |
-
# version location specification; This defaults
|
39 |
-
# to migration/versions. When using multiple version
|
40 |
-
# directories, initial revisions must be specified with --version-path.
|
41 |
-
# The path separator used here should be the separator specified by "version_path_separator" below.
|
42 |
-
# version_locations = %(here)s/bar:%(here)s/bat:migration/versions
|
43 |
-
|
44 |
-
# version path separator; As mentioned above, this is the character used to split
|
45 |
-
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
46 |
-
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
47 |
-
# Valid values for version_path_separator are:
|
48 |
-
#
|
49 |
-
# version_path_separator = :
|
50 |
-
# version_path_separator = ;
|
51 |
-
# version_path_separator = space
|
52 |
-
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
53 |
-
|
54 |
-
# set to 'true' to search source files recursively
|
55 |
-
# in each "version_locations" directory
|
56 |
-
# new in Alembic version 1.10
|
57 |
-
# recursive_version_locations = false
|
58 |
-
|
59 |
-
# the output encoding used when revision files
|
60 |
-
# are written from script.py.mako
|
61 |
-
# output_encoding = utf-8
|
62 |
-
|
63 |
-
sqlalchemy.url = driver://user:pass@localhost/dbname
|
64 |
-
|
65 |
-
|
66 |
-
[post_write_hooks]
|
67 |
-
# post_write_hooks defines scripts or Python functions that are run
|
68 |
-
# on newly generated revision scripts. See the documentation for further
|
69 |
-
# detail and examples
|
70 |
-
|
71 |
-
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
72 |
-
# hooks = black
|
73 |
-
# black.type = console_scripts
|
74 |
-
# black.entrypoint = black
|
75 |
-
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
76 |
-
|
77 |
-
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
78 |
-
# hooks = ruff
|
79 |
-
# ruff.type = exec
|
80 |
-
# ruff.executable = %(here)s/.venv/bin/ruff
|
81 |
-
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
82 |
-
|
83 |
-
# Logging configuration
|
84 |
-
[loggers]
|
85 |
-
keys = root,sqlalchemy,alembic
|
86 |
-
|
87 |
-
[handlers]
|
88 |
-
keys = console
|
89 |
-
|
90 |
-
[formatters]
|
91 |
-
keys = generic
|
92 |
-
|
93 |
-
[logger_root]
|
94 |
-
level = WARN
|
95 |
-
handlers = console
|
96 |
-
qualname =
|
97 |
-
|
98 |
-
[logger_sqlalchemy]
|
99 |
-
level = WARN
|
100 |
-
handlers =
|
101 |
-
qualname = sqlalchemy.engine
|
102 |
-
|
103 |
-
[logger_alembic]
|
104 |
-
level = INFO
|
105 |
-
handlers =
|
106 |
-
qualname = alembic
|
107 |
-
|
108 |
-
[handler_console]
|
109 |
-
class = StreamHandler
|
110 |
-
args = (sys.stderr,)
|
111 |
-
level = NOTSET
|
112 |
-
formatter = generic
|
113 |
-
|
114 |
-
[formatter_generic]
|
115 |
-
format = %(levelname)-5.5s [%(name)s] %(message)s
|
116 |
-
datefmt = %H:%M:%S
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/routers/chat.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
from pydantic import BaseModel
|
2 |
from typing import List, Any, Optional, Dict, Tuple
|
3 |
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
4 |
from llama_index.core.chat_engine.types import BaseChatEngine
|
@@ -19,8 +19,9 @@ class _Message(BaseModel):
|
|
19 |
|
20 |
class _ChatData(BaseModel):
|
21 |
messages: List[_Message]
|
22 |
-
|
23 |
-
|
|
|
24 |
"example": {
|
25 |
"messages": [
|
26 |
{
|
@@ -30,7 +31,6 @@ class _ChatData(BaseModel):
|
|
30 |
]
|
31 |
}
|
32 |
}
|
33 |
-
)
|
34 |
|
35 |
|
36 |
class _SourceNodes(BaseModel):
|
@@ -121,7 +121,12 @@ async def chat(
|
|
121 |
yield VercelStreamResponse.convert_data(
|
122 |
{
|
123 |
"type": "sources",
|
124 |
-
"data": {
|
|
|
|
|
|
|
|
|
|
|
125 |
}
|
126 |
)
|
127 |
|
|
|
1 |
+
from pydantic import BaseModel
|
2 |
from typing import List, Any, Optional, Dict, Tuple
|
3 |
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
4 |
from llama_index.core.chat_engine.types import BaseChatEngine
|
|
|
19 |
|
20 |
class _ChatData(BaseModel):
|
21 |
messages: List[_Message]
|
22 |
+
|
23 |
+
class Config:
|
24 |
+
json_schema_extra = {
|
25 |
"example": {
|
26 |
"messages": [
|
27 |
{
|
|
|
31 |
]
|
32 |
}
|
33 |
}
|
|
|
34 |
|
35 |
|
36 |
class _SourceNodes(BaseModel):
|
|
|
121 |
yield VercelStreamResponse.convert_data(
|
122 |
{
|
123 |
"type": "sources",
|
124 |
+
"data": {
|
125 |
+
"nodes": [
|
126 |
+
_SourceNodes.from_source_node(node).dict()
|
127 |
+
for node in response.source_nodes
|
128 |
+
]
|
129 |
+
},
|
130 |
}
|
131 |
)
|
132 |
|
app/api/routers/file_upload.py
DELETED
@@ -1,43 +0,0 @@
|
|
1 |
-
from typing import Annotated
|
2 |
-
from fastapi import APIRouter, UploadFile, Depends
|
3 |
-
from app.categorization.file_processing import process_file, save_results
|
4 |
-
from app.schema.index import FileUploadCreate
|
5 |
-
import asyncio
|
6 |
-
import os
|
7 |
-
import csv
|
8 |
-
|
9 |
-
from app.engine.postgresdb import get_db_session
|
10 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
11 |
-
|
12 |
-
file_upload_router = r = APIRouter(prefix="/api/v1/file_upload", tags=["file_upload"])
|
13 |
-
|
14 |
-
@r.post(
|
15 |
-
"/",
|
16 |
-
responses={
|
17 |
-
200: {"description": "File successfully uploaded"},
|
18 |
-
400: {"description": "Bad request"},
|
19 |
-
500: {"description": "Internal server error"},
|
20 |
-
},
|
21 |
-
)
|
22 |
-
async def create_file(input_file: UploadFile, db: AsyncSession = Depends(get_db_session)):
|
23 |
-
try:
|
24 |
-
# Create directory to store all uploaded .csv files
|
25 |
-
file_upload_directory_path = "data/tx_data/input"
|
26 |
-
if not os.path.exists(file_upload_directory_path):
|
27 |
-
os.makedirs(file_upload_directory_path)
|
28 |
-
|
29 |
-
# Write items of .csv filte to directory
|
30 |
-
with open(os.path.join(file_upload_directory_path, input_file.filename)) as output_file:
|
31 |
-
[output_file.write(" ".join(row)+'\n') for row in csv.reader(input_file)]
|
32 |
-
output_file.close()
|
33 |
-
|
34 |
-
# With the newly created file and it's path, process and save it for embedding
|
35 |
-
processed_file = process_file(os.path.realpath(input_file.filename))
|
36 |
-
result = await asyncio.gather(processed_file)
|
37 |
-
await save_results(db, result)
|
38 |
-
|
39 |
-
except Exception:
|
40 |
-
return {"message": "There was an error uploading this file. Ensure you have a .csv file with the following columns:"
|
41 |
-
"\n transaction_date, type, category, name_description, amount"}
|
42 |
-
|
43 |
-
return {"message": f"Successfully uploaded {input_file.filename}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/routers/income_statement.py
DELETED
@@ -1,64 +0,0 @@
|
|
1 |
-
from typing import List
|
2 |
-
from fastapi import APIRouter, Depends, HTTPException, status
|
3 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
4 |
-
from app.model.transaction import Transaction as TransactionModel
|
5 |
-
from app.model.income_statement import IncomeStatement as IncomeStatementModel
|
6 |
-
from app.schema.index import IncomeStatementCreateRequest, IncomeStatementResponse
|
7 |
-
from app.engine.postgresdb import get_db_session
|
8 |
-
from app.service.income_statement import call_llm_to_create_income_statement
|
9 |
-
|
10 |
-
income_statement_router = r = APIRouter(prefix="/api/v1/income_statement", tags=["income_statement"])
|
11 |
-
|
12 |
-
|
13 |
-
@r.post(
|
14 |
-
"/",
|
15 |
-
responses={
|
16 |
-
200: {"description": "New transaction created"},
|
17 |
-
400: {"description": "Bad request"},
|
18 |
-
500: {"description": "Internal server error"},
|
19 |
-
},
|
20 |
-
)
|
21 |
-
async def create_income_statement(payload: IncomeStatementCreateRequest, db: AsyncSession = Depends(get_db_session)) -> None:
|
22 |
-
try:
|
23 |
-
await call_llm_to_create_income_statement(payload, db)
|
24 |
-
|
25 |
-
except Exception as e:
|
26 |
-
raise HTTPException(status_code=500, detail=str(e))
|
27 |
-
|
28 |
-
|
29 |
-
@r.get(
|
30 |
-
"/user/{user_id}",
|
31 |
-
response_model=List[IncomeStatementResponse],
|
32 |
-
responses={
|
33 |
-
200: {"description": "New user created"},
|
34 |
-
400: {"description": "Bad request"},
|
35 |
-
204: {"description": "No content"},
|
36 |
-
500: {"description": "Internal server error"},
|
37 |
-
},
|
38 |
-
)
|
39 |
-
async def get_income_statements(
|
40 |
-
user_id: int, db: AsyncSession = Depends(get_db_session)
|
41 |
-
) -> List[IncomeStatementResponse]:
|
42 |
-
"""
|
43 |
-
Retrieve all income statements.
|
44 |
-
"""
|
45 |
-
result = await IncomeStatementModel.get_by_user(db, user_id)
|
46 |
-
if len(result) == 0:
|
47 |
-
raise HTTPException(status_code=status.HTTP_204_NO_CONTENT, detail="No income statements found for this user")
|
48 |
-
return result
|
49 |
-
|
50 |
-
|
51 |
-
@r.get(
|
52 |
-
"/report/{report_id}",
|
53 |
-
response_model=IncomeStatementResponse,
|
54 |
-
responses={
|
55 |
-
200: {"description": "Income statement found"},
|
56 |
-
404: {"description": "Income statement not found"},
|
57 |
-
500: {"description": "Internal server error"},
|
58 |
-
},
|
59 |
-
)
|
60 |
-
async def get_income_statement(report_id: int, db: AsyncSession = Depends(get_db_session)) -> IncomeStatementResponse:
|
61 |
-
income_statement = await IncomeStatementModel.get(db, id=report_id)
|
62 |
-
if not income_statement:
|
63 |
-
raise HTTPException(status_code=404, detail="Income statement not found")
|
64 |
-
return income_statement
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/routers/transaction.py
DELETED
@@ -1,30 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
from typing import List
|
3 |
-
from fastapi import APIRouter, Depends, HTTPException, status
|
4 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
5 |
-
from app.model.transaction import Transaction as TransactionModel
|
6 |
-
from app.schema.index import TransactionResponse
|
7 |
-
from app.engine.postgresdb import get_db_session
|
8 |
-
|
9 |
-
transaction_router = r = APIRouter(prefix="/api/v1/transactions", tags=["transactions"])
|
10 |
-
|
11 |
-
|
12 |
-
@r.get(
|
13 |
-
"/{user_id}",
|
14 |
-
response_model=List[TransactionResponse],
|
15 |
-
responses={
|
16 |
-
200: {"description": "New user created"},
|
17 |
-
400: {"description": "Bad request"},
|
18 |
-
204: {"description": "No content"},
|
19 |
-
500: {"description": "Internal server error"},
|
20 |
-
},
|
21 |
-
)
|
22 |
-
async def get_transactions(user_id: int, db: AsyncSession = Depends(get_db_session)):
|
23 |
-
"""
|
24 |
-
Retrieve all transactions.
|
25 |
-
"""
|
26 |
-
result = await TransactionModel.get_by_user(db, user_id)
|
27 |
-
all_rows = result.all()
|
28 |
-
if len(all_rows) == 0:
|
29 |
-
raise HTTPException(status_code=status.HTTP_204_NO_CONTENT, detail="No transactions found for this user")
|
30 |
-
return all_rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/routers/user.py
DELETED
@@ -1,89 +0,0 @@
|
|
1 |
-
import logging
|
2 |
-
from fastapi import APIRouter, Depends, HTTPException
|
3 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
4 |
-
|
5 |
-
from app.engine.postgresdb import get_db_session
|
6 |
-
from app.schema.index import UserCreate, User as UserSchema, UserResponse, UserUpdate
|
7 |
-
from app.model.user import User as UserModel
|
8 |
-
|
9 |
-
|
10 |
-
user_router = r = APIRouter(prefix="/api/v1/users", tags=["users"])
|
11 |
-
logger = logging.getLogger(__name__)
|
12 |
-
|
13 |
-
|
14 |
-
@r.post(
|
15 |
-
"/",
|
16 |
-
response_model=UserResponse,
|
17 |
-
responses={
|
18 |
-
200: {"description": "New user created"},
|
19 |
-
400: {"description": "Bad request"},
|
20 |
-
409: {"description": "Conflict"},
|
21 |
-
500: {"description": "Internal server error"},
|
22 |
-
},
|
23 |
-
)
|
24 |
-
async def create_user(user: UserCreate, db: AsyncSession = Depends(get_db_session)):
|
25 |
-
try:
|
26 |
-
db_user = await UserModel.get(db, email=user.email)
|
27 |
-
if db_user and not db_user.is_deleted:
|
28 |
-
raise HTTPException(status_code=409, detail="User already exists")
|
29 |
-
|
30 |
-
await UserModel.create(db, **user.model_dump())
|
31 |
-
user = await UserModel.get(db, email=user.email)
|
32 |
-
return user
|
33 |
-
except Exception as e:
|
34 |
-
raise HTTPException(status_code=500, detail=str(e))
|
35 |
-
|
36 |
-
|
37 |
-
@r.get(
|
38 |
-
"/{email}",
|
39 |
-
response_model=UserResponse,
|
40 |
-
responses={
|
41 |
-
200: {"description": "User found"},
|
42 |
-
404: {"description": "User not found"},
|
43 |
-
500: {"description": "Internal server error"},
|
44 |
-
},
|
45 |
-
)
|
46 |
-
async def get_user(email: str, db: AsyncSession = Depends(get_db_session)):
|
47 |
-
user = await UserModel.get(db, email=email)
|
48 |
-
if not user:
|
49 |
-
raise HTTPException(status_code=404, detail="User not found")
|
50 |
-
return user
|
51 |
-
|
52 |
-
|
53 |
-
@r.put(
|
54 |
-
"/{email}",
|
55 |
-
response_model=UserResponse,
|
56 |
-
responses={
|
57 |
-
200: {"description": "User updated"},
|
58 |
-
404: {"description": "User not found"},
|
59 |
-
500: {"description": "Internal server error"},
|
60 |
-
},
|
61 |
-
)
|
62 |
-
async def update_user(email: str, user_payload: UserUpdate, db: AsyncSession = Depends(get_db_session)):
|
63 |
-
try:
|
64 |
-
user = await UserModel.get(db, email=email)
|
65 |
-
if not user:
|
66 |
-
raise HTTPException(status_code=404, detail="User not found")
|
67 |
-
await UserModel.update(db, id=user.id, **user_payload.model_dump())
|
68 |
-
user = await UserModel.get(db, email=email)
|
69 |
-
return user
|
70 |
-
except Exception as e:
|
71 |
-
raise HTTPException(status_code=500, detail=str(e))
|
72 |
-
|
73 |
-
|
74 |
-
@r.delete(
|
75 |
-
"/{email}",
|
76 |
-
response_model=UserResponse,
|
77 |
-
responses={
|
78 |
-
200: {"description": "User deleted"},
|
79 |
-
404: {"description": "User not found"},
|
80 |
-
500: {"description": "Internal server error"},
|
81 |
-
},
|
82 |
-
)
|
83 |
-
async def delete_user(email: str, db: AsyncSession = Depends(get_db_session)):
|
84 |
-
user = await UserModel.get(db, email=email)
|
85 |
-
if not user:
|
86 |
-
raise HTTPException(status_code=404, detail="User not found")
|
87 |
-
await UserModel.delete(db, email=email)
|
88 |
-
user = await UserModel.get(db, email=email)
|
89 |
-
return user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/categorization/file_processing.py
CHANGED
@@ -11,11 +11,6 @@ from dateparser import parse
|
|
11 |
|
12 |
from app.categorization.categorizer_list import categorize_list
|
13 |
from app.categorization.config import RESULT_OUTPUT_FILE, CATEGORY_REFERENCE_OUTPUT_FILE
|
14 |
-
from app.model.transaction import Transaction
|
15 |
-
from app.schema.index import TransactionCreate
|
16 |
-
|
17 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
18 |
-
|
19 |
|
20 |
# Read file and process it (e.g. categorize transactions)
|
21 |
async def process_file(file_path: str) -> Dict[str, Union[str, pd.DataFrame]]:
|
@@ -27,27 +22,28 @@ async def process_file(file_path: str) -> Dict[str, Union[str, pd.DataFrame]]:
|
|
27 |
|
28 |
Returns:
|
29 |
Dict[str, Union[str, pd.DataFrame]]: Dictionary containing the file name, processed output, and error information if any
|
30 |
-
"""
|
31 |
|
32 |
file_name = os.path.basename(file_path)
|
33 |
-
result
|
34 |
try:
|
35 |
-
# Read file into standardized tx format: source, date, type, category, description, amount
|
36 |
tx_list = standardize_csv_file(file_path)
|
37 |
|
38 |
# Categorize transactions
|
39 |
-
result[
|
40 |
-
print(f
|
41 |
|
42 |
except Exception as e:
|
43 |
# Return an error indicator and exception info
|
44 |
logging.log(logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}")
|
45 |
-
print(f
|
46 |
-
result[
|
47 |
-
|
48 |
return result
|
49 |
|
50 |
|
|
|
51 |
def standardize_csv_file(file_path: str) -> pd.DataFrame:
|
52 |
"""
|
53 |
Read and prepare the data from the input file.
|
@@ -59,21 +55,21 @@ def standardize_csv_file(file_path: str) -> pd.DataFrame:
|
|
59 |
pd.DataFrame: Prepared transaction data.
|
60 |
"""
|
61 |
|
62 |
-
tx_list = pd.read_csv(file_path, index_col=False)
|
63 |
-
tx_list.attrs[
|
64 |
tx_list.columns = tx_list.columns.str.lower().str.strip()
|
65 |
|
66 |
# Standardize dates to YYYY/MM/DD format
|
67 |
-
tx_list[
|
68 |
|
69 |
# Add source and reindex to desired tx format; category column is new and therefore empty
|
70 |
-
tx_list.loc[:,
|
71 |
-
tx_list = tx_list.reindex(columns=[
|
72 |
|
73 |
return tx_list
|
74 |
|
75 |
|
76 |
-
|
77 |
"""
|
78 |
Merge all interim results in the input folder and write the merged results to the output file.
|
79 |
|
@@ -91,33 +87,29 @@ async def save_results(db: AsyncSession, results: List) -> None:
|
|
91 |
ko_files = []
|
92 |
error_messages = []
|
93 |
|
94 |
-
col_list = [
|
95 |
tx_list = pd.DataFrame(columns=col_list)
|
96 |
for result in results:
|
97 |
-
if not result[
|
98 |
-
ok_files.append(result[
|
99 |
-
result_df = result[
|
100 |
result_df.columns = col_list
|
101 |
tx_list = pd.concat([tx_list, result_df], ignore_index=True)
|
102 |
else:
|
103 |
-
ko_files.append(result[
|
104 |
-
error_messages.append(f"{result['file_name']}: {result['error']}")
|
105 |
|
106 |
-
#
|
107 |
-
|
108 |
-
txn_list_to_save = [TransactionCreate(**row.to_dict(), user_id=1) for _, row in tx_list.iterrows()]
|
109 |
-
await Transaction.bulk_create(db, txn_list_to_save)
|
110 |
|
111 |
-
new_ref_data = tx_list[[
|
112 |
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
113 |
# If it exists, add master file to interim results
|
114 |
-
old_ref_data = pd.read_csv(CATEGORY_REFERENCE_OUTPUT_FILE, names=[
|
115 |
new_ref_data = pd.concat([old_ref_data, new_ref_data], ignore_index=True)
|
116 |
-
|
117 |
# Drop duplicates, sort, and write to create new Master File
|
118 |
-
new_ref_data.drop_duplicates(subset=[
|
119 |
-
CATEGORY_REFERENCE_OUTPUT_FILE, mode="w", index=False, header=True
|
120 |
-
)
|
121 |
|
122 |
# Summarize results
|
123 |
print(f"\nProcessed {len(results)} files: {len(ok_files)} successful, {len(ko_files)} with errors\n")
|
@@ -125,4 +117,4 @@ async def save_results(db: AsyncSession, results: List) -> None:
|
|
125 |
print(f"Errors in the following files:")
|
126 |
for message in error_messages:
|
127 |
print(f" {message}")
|
128 |
-
print(
|
|
|
11 |
|
12 |
from app.categorization.categorizer_list import categorize_list
|
13 |
from app.categorization.config import RESULT_OUTPUT_FILE, CATEGORY_REFERENCE_OUTPUT_FILE
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
# Read file and process it (e.g. categorize transactions)
|
16 |
async def process_file(file_path: str) -> Dict[str, Union[str, pd.DataFrame]]:
|
|
|
22 |
|
23 |
Returns:
|
24 |
Dict[str, Union[str, pd.DataFrame]]: Dictionary containing the file name, processed output, and error information if any
|
25 |
+
"""
|
26 |
|
27 |
file_name = os.path.basename(file_path)
|
28 |
+
result= {'file_name': file_name, 'output': pd.DataFrame(), 'error': ''}
|
29 |
try:
|
30 |
+
# Read file into standardized tx format: source, date, type, category, description, amount
|
31 |
tx_list = standardize_csv_file(file_path)
|
32 |
|
33 |
# Categorize transactions
|
34 |
+
result['output'] = await categorize_list(tx_list)
|
35 |
+
print(f'File processed sucessfully: {file_name}')
|
36 |
|
37 |
except Exception as e:
|
38 |
# Return an error indicator and exception info
|
39 |
logging.log(logging.ERROR, f"| File: {file_name} | Unexpected Error: {e}")
|
40 |
+
print(f'ERROR processing file {file_name}: {e}')
|
41 |
+
result['error'] = str(e)
|
42 |
+
|
43 |
return result
|
44 |
|
45 |
|
46 |
+
|
47 |
def standardize_csv_file(file_path: str) -> pd.DataFrame:
|
48 |
"""
|
49 |
Read and prepare the data from the input file.
|
|
|
55 |
pd.DataFrame: Prepared transaction data.
|
56 |
"""
|
57 |
|
58 |
+
tx_list = pd.read_csv(file_path, index_col=False)
|
59 |
+
tx_list.attrs['file_name'] = file_path
|
60 |
tx_list.columns = tx_list.columns.str.lower().str.strip()
|
61 |
|
62 |
# Standardize dates to YYYY/MM/DD format
|
63 |
+
tx_list['date'] = pd.to_datetime(tx_list['date']).dt.strftime('%Y/%m/%d')
|
64 |
|
65 |
# Add source and reindex to desired tx format; category column is new and therefore empty
|
66 |
+
tx_list.loc[:, 'source'] = os.path.basename(file_path)
|
67 |
+
tx_list = tx_list.reindex(columns=['date', 'expense/income', 'category', 'name/description', 'amount'])
|
68 |
|
69 |
return tx_list
|
70 |
|
71 |
|
72 |
+
def save_results(results: List) -> None:
|
73 |
"""
|
74 |
Merge all interim results in the input folder and write the merged results to the output file.
|
75 |
|
|
|
87 |
ko_files = []
|
88 |
error_messages = []
|
89 |
|
90 |
+
col_list = ['date', 'expense/income', 'category', 'name/description', 'amount']
|
91 |
tx_list = pd.DataFrame(columns=col_list)
|
92 |
for result in results:
|
93 |
+
if not result['error']:
|
94 |
+
ok_files.append(result['file_name'])
|
95 |
+
result_df = result['output']
|
96 |
result_df.columns = col_list
|
97 |
tx_list = pd.concat([tx_list, result_df], ignore_index=True)
|
98 |
else:
|
99 |
+
ko_files.append(result['file_name'])
|
100 |
+
error_messages.append(f"{result['file_name']}: {result['error']}")
|
101 |
|
102 |
+
# Write contents to output file (based on file type)
|
103 |
+
tx_list.to_csv(RESULT_OUTPUT_FILE, mode="a", index=False, header=not os.path.exists(RESULT_OUTPUT_FILE))
|
|
|
|
|
104 |
|
105 |
+
new_ref_data = tx_list[['name/description', 'category']]
|
106 |
if os.path.exists(CATEGORY_REFERENCE_OUTPUT_FILE):
|
107 |
# If it exists, add master file to interim results
|
108 |
+
old_ref_data = pd.read_csv(CATEGORY_REFERENCE_OUTPUT_FILE, names=['name/description', 'category'], header=0)
|
109 |
new_ref_data = pd.concat([old_ref_data, new_ref_data], ignore_index=True)
|
110 |
+
|
111 |
# Drop duplicates, sort, and write to create new Master File
|
112 |
+
new_ref_data.drop_duplicates(subset=['name/description']).sort_values(by=['name/description']).to_csv(CATEGORY_REFERENCE_OUTPUT_FILE, mode="w", index=False, header=True)
|
|
|
|
|
113 |
|
114 |
# Summarize results
|
115 |
print(f"\nProcessed {len(results)} files: {len(ok_files)} successful, {len(ko_files)} with errors\n")
|
|
|
117 |
print(f"Errors in the following files:")
|
118 |
for message in error_messages:
|
119 |
print(f" {message}")
|
120 |
+
print('\n')
|
app/engine/postgresdb.py
DELETED
@@ -1,81 +0,0 @@
|
|
1 |
-
import logging
|
2 |
-
import contextlib
|
3 |
-
from typing import Any, AsyncIterator
|
4 |
-
|
5 |
-
from sqlalchemy.ext.asyncio import (
|
6 |
-
AsyncConnection,
|
7 |
-
AsyncSession,
|
8 |
-
AsyncEngine,
|
9 |
-
async_sessionmaker,
|
10 |
-
create_async_engine,
|
11 |
-
)
|
12 |
-
from sqlalchemy.orm import declarative_base
|
13 |
-
|
14 |
-
from config.index import config as env
|
15 |
-
|
16 |
-
logger = logging.getLogger(__name__)
|
17 |
-
|
18 |
-
Base = declarative_base()
|
19 |
-
|
20 |
-
|
21 |
-
class PostgresDatabase:
|
22 |
-
|
23 |
-
def __init__(self):
|
24 |
-
self._engine: AsyncEngine | None = None
|
25 |
-
self._sessionmaker: async_sessionmaker | None = None
|
26 |
-
|
27 |
-
def init(self, host: str, engine_kwargs: dict[str, Any] = {}):
|
28 |
-
self._engine = create_async_engine(host, **engine_kwargs)
|
29 |
-
self._sessionmaker = async_sessionmaker(autocommit=False, bind=self._engine)
|
30 |
-
|
31 |
-
async def close(self):
|
32 |
-
if self._engine is None:
|
33 |
-
raise Exception("DatabaseSessionManager is not initialized")
|
34 |
-
await self._engine.dispose()
|
35 |
-
|
36 |
-
self._engine = None
|
37 |
-
self._sessionmaker = None
|
38 |
-
|
39 |
-
@contextlib.asynccontextmanager
|
40 |
-
async def connect(self) -> AsyncIterator[AsyncConnection]:
|
41 |
-
if self._engine is None:
|
42 |
-
raise Exception("DatabaseSessionManager is not initialized")
|
43 |
-
|
44 |
-
async with self._engine.begin() as connection:
|
45 |
-
try:
|
46 |
-
yield connection
|
47 |
-
except Exception:
|
48 |
-
await connection.rollback()
|
49 |
-
raise
|
50 |
-
|
51 |
-
@contextlib.asynccontextmanager
|
52 |
-
async def session(self) -> AsyncIterator[AsyncSession]:
|
53 |
-
if self._sessionmaker is None:
|
54 |
-
raise Exception("DatabaseSessionManager is not initialized")
|
55 |
-
|
56 |
-
session = self._sessionmaker()
|
57 |
-
try:
|
58 |
-
yield session
|
59 |
-
except Exception:
|
60 |
-
await session.rollback()
|
61 |
-
raise
|
62 |
-
finally:
|
63 |
-
await session.close()
|
64 |
-
|
65 |
-
def get_engine(self):
|
66 |
-
return self._engine
|
67 |
-
|
68 |
-
# Used for testing
|
69 |
-
async def create_all(self, connection: AsyncConnection):
|
70 |
-
await connection.run_sync(Base.metadata.create_all)
|
71 |
-
|
72 |
-
async def drop_all(self, connection: AsyncConnection):
|
73 |
-
await connection.run_sync(Base.metadata.drop_all)
|
74 |
-
|
75 |
-
|
76 |
-
postgresdb = PostgresDatabase()
|
77 |
-
|
78 |
-
|
79 |
-
async def get_db_session():
|
80 |
-
async with postgresdb.session() as session:
|
81 |
-
yield session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/model/base.py
DELETED
@@ -1,15 +0,0 @@
|
|
1 |
-
from datetime import datetime, timezone
|
2 |
-
from sqlalchemy import DateTime
|
3 |
-
from sqlalchemy.orm import Mapped, mapped_column
|
4 |
-
|
5 |
-
from app.engine.postgresdb import Base
|
6 |
-
|
7 |
-
|
8 |
-
class BaseModel:
|
9 |
-
id: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True)
|
10 |
-
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc))
|
11 |
-
updated_at: Mapped[datetime] = mapped_column(
|
12 |
-
DateTime(timezone=True),
|
13 |
-
default=datetime.now(timezone.utc),
|
14 |
-
onupdate=datetime.now(timezone.utc),
|
15 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/model/income_statement.py
DELETED
@@ -1,46 +0,0 @@
|
|
1 |
-
from datetime import datetime
|
2 |
-
from typing import Any, Dict, List
|
3 |
-
from sqlalchemy import ForeignKey
|
4 |
-
from sqlalchemy.sql import expression as sql
|
5 |
-
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
6 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
7 |
-
from sqlalchemy.dialects.postgresql import JSON
|
8 |
-
|
9 |
-
from app.model.base import BaseModel
|
10 |
-
from app.engine.postgresdb import Base
|
11 |
-
from app.schema.index import IncomeStatement as IncomeStatementSchema
|
12 |
-
|
13 |
-
|
14 |
-
class IncomeStatement(Base, BaseModel):
|
15 |
-
__tablename__ = "income_statement"
|
16 |
-
|
17 |
-
date_from: Mapped[datetime]
|
18 |
-
date_to: Mapped[datetime]
|
19 |
-
income: Mapped[Dict] = mapped_column(JSON, nullable=True)
|
20 |
-
expenses: Mapped[Dict] = mapped_column(JSON, nullable=True)
|
21 |
-
|
22 |
-
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
23 |
-
user = relationship("User", back_populates="income_statements")
|
24 |
-
|
25 |
-
def __str__(self) -> str:
|
26 |
-
return f"IncomeStatement(id={self.id}, user_id={self.user_id}, date_from={self.date_from}, date_to={self.date_to}, income={self.income}, expenses={self.expenses})"
|
27 |
-
|
28 |
-
@classmethod
|
29 |
-
async def create(cls: "type[IncomeStatement]", db: AsyncSession, **kwargs: IncomeStatementSchema) -> "IncomeStatement":
|
30 |
-
income_statement = cls(**kwargs)
|
31 |
-
db.add(income_statement)
|
32 |
-
await db.commit()
|
33 |
-
await db.refresh(income_statement)
|
34 |
-
return income_statement
|
35 |
-
|
36 |
-
@classmethod
|
37 |
-
async def get_by_user(cls: "type[IncomeStatement]", db: AsyncSession, user_id: int) -> "List[IncomeStatement]":
|
38 |
-
query = sql.select(cls).where(cls.user_id == user_id)
|
39 |
-
income_statements = await db.scalars(query)
|
40 |
-
return income_statements.all()
|
41 |
-
|
42 |
-
@classmethod
|
43 |
-
async def get(cls: "type[IncomeStatement]", db: AsyncSession, id: int) -> "IncomeStatement":
|
44 |
-
query = sql.select(cls).where(cls.id == id)
|
45 |
-
income_statement = await db.scalar(query)
|
46 |
-
return income_statement
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/model/transaction.py
DELETED
@@ -1,62 +0,0 @@
|
|
1 |
-
from datetime import datetime
|
2 |
-
from typing import List
|
3 |
-
from sqlalchemy import ForeignKey
|
4 |
-
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
5 |
-
from sqlalchemy.sql import expression as sql
|
6 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
7 |
-
|
8 |
-
from app.model.base import BaseModel
|
9 |
-
from app.engine.postgresdb import Base
|
10 |
-
from app.schema.index import TransactionCreate
|
11 |
-
|
12 |
-
|
13 |
-
class Transaction(Base, BaseModel):
|
14 |
-
__tablename__ = "transactions"
|
15 |
-
transaction_date: Mapped[datetime]
|
16 |
-
category: Mapped[str]
|
17 |
-
name_description: Mapped[str]
|
18 |
-
amount: Mapped[float]
|
19 |
-
type: Mapped[str]
|
20 |
-
|
21 |
-
user_id = mapped_column(ForeignKey("users.id"))
|
22 |
-
user = relationship("User", back_populates="transactions")
|
23 |
-
|
24 |
-
def __str__(self) -> str:
|
25 |
-
return f"{self.transaction_date}, {self.category}, {self.name_description}, {self.amount}, {self.type}"
|
26 |
-
|
27 |
-
@classmethod
|
28 |
-
async def create(cls: "type[Transaction]", db: AsyncSession, **kwargs) -> "Transaction":
|
29 |
-
query = sql.insert(cls).values(**kwargs)
|
30 |
-
transactions = await db.execute(query)
|
31 |
-
transaction = transactions.first()
|
32 |
-
await db.commit()
|
33 |
-
return transaction
|
34 |
-
|
35 |
-
@classmethod
|
36 |
-
async def bulk_create(cls: "type[Transaction]", db: AsyncSession, transactions: List[TransactionCreate]) -> None:
|
37 |
-
values = [transaction.model_dump() for transaction in transactions]
|
38 |
-
query = sql.insert(cls).values(values)
|
39 |
-
await db.execute(query)
|
40 |
-
await db.commit()
|
41 |
-
|
42 |
-
@classmethod
|
43 |
-
async def update(cls: "type[Transaction]", db: AsyncSession, id: int, **kwargs) -> "Transaction":
|
44 |
-
query = sql.update(cls).where(cls.id == id).values(**kwargs).execution_options(synchronize_session="fetch")
|
45 |
-
transactions = await db.scalars(query)
|
46 |
-
transaction = transactions.first()
|
47 |
-
await db.commit()
|
48 |
-
return transaction
|
49 |
-
|
50 |
-
@classmethod
|
51 |
-
async def get_by_user(cls: "type[Transaction]", db: AsyncSession, user_id: int) -> "List[Transaction]":
|
52 |
-
query = sql.select(cls).where(cls.user_id == user_id)
|
53 |
-
transactions = await db.scalars(query)
|
54 |
-
return transactions
|
55 |
-
|
56 |
-
@classmethod
|
57 |
-
async def get_by_user_between_dates(
|
58 |
-
cls: "type[Transaction]", db: AsyncSession, user_id: int, start_date: datetime, end_date: datetime
|
59 |
-
) -> "List[Transaction]":
|
60 |
-
query = sql.select(cls).where(cls.user_id == user_id).where(cls.transaction_date.between(start_date, end_date))
|
61 |
-
transactions = await db.scalars(query)
|
62 |
-
return transactions.all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/model/user.py
DELETED
@@ -1,110 +0,0 @@
|
|
1 |
-
import logging
|
2 |
-
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
3 |
-
from sqlalchemy.sql import expression as sql
|
4 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
5 |
-
|
6 |
-
from app.model.base import BaseModel
|
7 |
-
from app.engine.postgresdb import Base
|
8 |
-
from app.schema.index import UserCreate, UserUpdate
|
9 |
-
|
10 |
-
logger = logging.getLogger(__name__)
|
11 |
-
|
12 |
-
|
13 |
-
class User(Base, BaseModel):
|
14 |
-
__tablename__ = "users"
|
15 |
-
|
16 |
-
name: Mapped[str]
|
17 |
-
email: Mapped[str]
|
18 |
-
hashed_password: Mapped[str]
|
19 |
-
is_deleted: Mapped[bool] = mapped_column(default=False)
|
20 |
-
|
21 |
-
transactions = relationship("Transaction", back_populates="user")
|
22 |
-
income_statements = relationship("IncomeStatement", back_populates="user")
|
23 |
-
|
24 |
-
@classmethod
|
25 |
-
async def create(cls: "type[User]", db: AsyncSession, **kwargs: UserCreate) -> "User":
|
26 |
-
"""
|
27 |
-
Creates a new user in the database.
|
28 |
-
|
29 |
-
Args:
|
30 |
-
cls (type[User]): The class object representing the User model.
|
31 |
-
db (AsyncSession): The asynchronous session object for interacting with the database.
|
32 |
-
**kwargs (UserCreate): The keyword arguments representing the user's attributes.
|
33 |
-
|
34 |
-
Returns:
|
35 |
-
User
|
36 |
-
"""
|
37 |
-
|
38 |
-
print(f"Creating user: {kwargs}")
|
39 |
-
# query = sql.insert(cls).values(**kwargs)
|
40 |
-
user = cls(name=kwargs["name"], email=kwargs["email"], hashed_password=kwargs["hashed_password"])
|
41 |
-
db.add(user)
|
42 |
-
await db.commit()
|
43 |
-
await db.refresh(user)
|
44 |
-
return user
|
45 |
-
|
46 |
-
@classmethod
|
47 |
-
async def update(cls: "type[User]", db: AsyncSession, id: int, **kwargs: UserUpdate) -> "User":
|
48 |
-
"""
|
49 |
-
Updates a user in the database with the given ID and keyword arguments.
|
50 |
-
|
51 |
-
Args:
|
52 |
-
cls (type[User]): The class object representing the User model.
|
53 |
-
db (AsyncSession): The asynchronous session object for interacting with the database.
|
54 |
-
id (int): The ID of the user to update.
|
55 |
-
**kwargs (UserUpdate): The keyword arguments representing the user's attributes to update.
|
56 |
-
|
57 |
-
Returns:
|
58 |
-
User
|
59 |
-
"""
|
60 |
-
|
61 |
-
query = sql.update(cls).where(cls.id == id).values(**kwargs).execution_options(synchronize_session="fetch")
|
62 |
-
results = await db.execute(query)
|
63 |
-
user = results.fetchone()
|
64 |
-
await db.commit()
|
65 |
-
return user
|
66 |
-
|
67 |
-
@classmethod
|
68 |
-
async def get(cls: "type[User]", db: AsyncSession, email: str) -> "User":
|
69 |
-
"""
|
70 |
-
Retrieves a user from the database based on their email.
|
71 |
-
|
72 |
-
Args:
|
73 |
-
cls (type[User]): The class object representing the User model.
|
74 |
-
db (AsyncSession): The asynchronous session object for interacting with the database.
|
75 |
-
email (str): The email of the user to retrieve.
|
76 |
-
|
77 |
-
Returns:
|
78 |
-
User
|
79 |
-
"""
|
80 |
-
|
81 |
-
logging.info(f"Getting user: {email}")
|
82 |
-
query = sql.select(cls).where(cls.email == email)
|
83 |
-
logging.info(f"Query: {query}")
|
84 |
-
users = await db.scalars(query)
|
85 |
-
logging.info(f"Users: {users}")
|
86 |
-
return users.first()
|
87 |
-
|
88 |
-
@classmethod
|
89 |
-
async def delete(cls: "type[User]", db: AsyncSession, email: str) -> "User":
|
90 |
-
"""
|
91 |
-
Deletes a user from the database based on their email.
|
92 |
-
|
93 |
-
Args:
|
94 |
-
cls (type[User]): The class object representing the User model.
|
95 |
-
db (AsyncSession): The asynchronous session object for interacting with the database.
|
96 |
-
email (str): The email of the user to delete.
|
97 |
-
|
98 |
-
Returns:
|
99 |
-
User
|
100 |
-
"""
|
101 |
-
query = (
|
102 |
-
sql.update(cls)
|
103 |
-
.where(cls.email == email)
|
104 |
-
.values(is_deleted=True)
|
105 |
-
.execution_options(synchronize_session="fetch")
|
106 |
-
)
|
107 |
-
result = await db.execute(query)
|
108 |
-
user = result.fetchone()
|
109 |
-
await db.commit()
|
110 |
-
return user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/schema/base.py
DELETED
@@ -1,14 +0,0 @@
|
|
1 |
-
from datetime import datetime
|
2 |
-
from pydantic import BaseModel, ConfigDict
|
3 |
-
|
4 |
-
|
5 |
-
class PydanticBaseModel(BaseModel):
|
6 |
-
pass
|
7 |
-
|
8 |
-
|
9 |
-
class BaseModel(PydanticBaseModel):
|
10 |
-
model_config = ConfigDict(from_attributes=True)
|
11 |
-
|
12 |
-
id: int
|
13 |
-
created_at: datetime
|
14 |
-
updated_at: datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/schema/index.py
DELETED
@@ -1,85 +0,0 @@
|
|
1 |
-
from enum import Enum
|
2 |
-
from datetime import datetime
|
3 |
-
from typing import Dict, List
|
4 |
-
from typing_extensions import TypedDict
|
5 |
-
|
6 |
-
from app.schema.base import BaseModel, PydanticBaseModel
|
7 |
-
|
8 |
-
|
9 |
-
class TransactionType(str, Enum):
|
10 |
-
INCOME = "income"
|
11 |
-
EXPENSE = "expense"
|
12 |
-
|
13 |
-
|
14 |
-
class UserCreate(PydanticBaseModel):
|
15 |
-
name: str
|
16 |
-
email: str
|
17 |
-
hashed_password: str
|
18 |
-
|
19 |
-
|
20 |
-
class UserUpdate(PydanticBaseModel):
|
21 |
-
name: str
|
22 |
-
email: str
|
23 |
-
hashed_password: str
|
24 |
-
|
25 |
-
|
26 |
-
class UserResponse(PydanticBaseModel):
|
27 |
-
id: int
|
28 |
-
name: str
|
29 |
-
email: str
|
30 |
-
is_deleted: bool
|
31 |
-
|
32 |
-
|
33 |
-
class User(BaseModel):
|
34 |
-
name: str
|
35 |
-
email: str
|
36 |
-
hashed_password: str
|
37 |
-
is_deleted: bool = False
|
38 |
-
transactions: "List[Transaction]" = []
|
39 |
-
|
40 |
-
|
41 |
-
class TransactionResponse(PydanticBaseModel):
|
42 |
-
transaction_date: datetime
|
43 |
-
category: str
|
44 |
-
name_description: str
|
45 |
-
amount: float
|
46 |
-
type: TransactionType
|
47 |
-
|
48 |
-
|
49 |
-
class TransactionCreate(TransactionResponse):
|
50 |
-
user_id: int
|
51 |
-
|
52 |
-
|
53 |
-
class Transaction(TransactionResponse):
|
54 |
-
user: User
|
55 |
-
|
56 |
-
|
57 |
-
class FileUploadCreate(PydanticBaseModel):
|
58 |
-
source: str
|
59 |
-
date: datetime
|
60 |
-
category: str
|
61 |
-
name_description: str
|
62 |
-
amount: float
|
63 |
-
type: str
|
64 |
-
|
65 |
-
|
66 |
-
class IncomeStatementCreateRequest(PydanticBaseModel):
|
67 |
-
user_id: int
|
68 |
-
date_from: datetime
|
69 |
-
date_to: datetime
|
70 |
-
|
71 |
-
|
72 |
-
class IncomeStatementDetail(TypedDict):
|
73 |
-
total: float
|
74 |
-
category_totals: List[Dict[str, str | float]]
|
75 |
-
|
76 |
-
class IncomeStatementLLMResponse(PydanticBaseModel):
|
77 |
-
income: IncomeStatementDetail
|
78 |
-
expenses: IncomeStatementDetail
|
79 |
-
|
80 |
-
|
81 |
-
class IncomeStatement(IncomeStatementCreateRequest, IncomeStatementLLMResponse):
|
82 |
-
pass
|
83 |
-
|
84 |
-
class IncomeStatementResponse(IncomeStatement):
|
85 |
-
id: int
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/service/income_statement.py
DELETED
@@ -1,37 +0,0 @@
|
|
1 |
-
import logging
|
2 |
-
from app.schema.index import IncomeStatement, IncomeStatementCreateRequest
|
3 |
-
from app.model.transaction import Transaction as TransactionModel
|
4 |
-
from app.model.income_statement import IncomeStatement as IncomeStatementModel
|
5 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
6 |
-
|
7 |
-
from app.service.llm import call_llm
|
8 |
-
|
9 |
-
|
10 |
-
async def call_llm_to_create_income_statement(payload: IncomeStatementCreateRequest, db: AsyncSession) -> None:
|
11 |
-
transactions = await TransactionModel.get_by_user_between_dates(
|
12 |
-
db, payload.user_id, payload.date_from, payload.date_to
|
13 |
-
)
|
14 |
-
|
15 |
-
if not transactions:
|
16 |
-
print("No transactions found")
|
17 |
-
return
|
18 |
-
|
19 |
-
response = await call_llm(transactions)
|
20 |
-
|
21 |
-
income = response.dict()['income']
|
22 |
-
expenses = response.dict()['expenses']
|
23 |
-
|
24 |
-
try:
|
25 |
-
income_statement_create_payload = IncomeStatement(
|
26 |
-
user_id=payload.user_id,
|
27 |
-
date_from=payload.date_from,
|
28 |
-
date_to=payload.date_to,
|
29 |
-
income=income,
|
30 |
-
expenses=expenses,
|
31 |
-
)
|
32 |
-
|
33 |
-
income_statement = await IncomeStatementModel.create(db, **income_statement_create_payload.model_dump())
|
34 |
-
print(f"Income statement created: {income_statement}")
|
35 |
-
except Exception as e:
|
36 |
-
print(e)
|
37 |
-
raise e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/service/llm.py
DELETED
@@ -1,55 +0,0 @@
|
|
1 |
-
from typing import List
|
2 |
-
from langchain_core.prompts import ChatPromptTemplate
|
3 |
-
from langchain_core.runnables.base import RunnableSequence
|
4 |
-
from langchain_openai import OpenAI
|
5 |
-
from langchain.globals import set_llm_cache
|
6 |
-
|
7 |
-
from app.model.transaction import Transaction
|
8 |
-
from app.schema.index import IncomeStatementLLMResponse
|
9 |
-
from config.index import config as env
|
10 |
-
|
11 |
-
from langchain_core.output_parsers import PydanticOutputParser
|
12 |
-
|
13 |
-
set_llm_cache(None)
|
14 |
-
|
15 |
-
def income_statement_prompt () -> ChatPromptTemplate:
|
16 |
-
context_str = """
|
17 |
-
You are an accountant skilled at organizing transactions from multiple different bank
|
18 |
-
accounts and credit card statements to prepare an income statement.
|
19 |
-
|
20 |
-
Input data is in the below csv format:
|
21 |
-
transaction_date, category, name_description, amount, type\n
|
22 |
-
{input_data_csv}
|
23 |
-
|
24 |
-
Your task is to prepare an income statement. The output should be in the following format: {format_instructions}
|
25 |
-
|
26 |
-
"""
|
27 |
-
prompt = ChatPromptTemplate.from_template(context_str)
|
28 |
-
return prompt
|
29 |
-
|
30 |
-
async def call_llm(inputData: List[Transaction]) -> str:
|
31 |
-
input_data_csv = '\n'.join(str(x) for x in inputData)
|
32 |
-
|
33 |
-
output_parser = PydanticOutputParser(pydantic_object=IncomeStatementLLMResponse)
|
34 |
-
|
35 |
-
prompt = income_statement_prompt().partial(format_instructions=output_parser.get_format_instructions())
|
36 |
-
|
37 |
-
llm = OpenAI(name='Income Statement Generation Bot',
|
38 |
-
api_key=env.OPENAI_API_KEY,
|
39 |
-
# cache=True,
|
40 |
-
temperature=0.7,
|
41 |
-
verbose=True)
|
42 |
-
|
43 |
-
try:
|
44 |
-
runnable_chain = RunnableSequence(prompt, llm, output_parser)
|
45 |
-
except Exception as e:
|
46 |
-
print(f"runnable_chain error: {str(e)}")
|
47 |
-
raise e
|
48 |
-
|
49 |
-
try:
|
50 |
-
output_chunks = runnable_chain.invoke({"input_data_csv": input_data_csv})
|
51 |
-
return output_chunks
|
52 |
-
|
53 |
-
except Exception as e:
|
54 |
-
print(f"runnable_chain.invoke error: {str(e)}")
|
55 |
-
raise e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/transactions_rag/categorize_transactions.ipynb
CHANGED
@@ -529,7 +529,7 @@
|
|
529 |
" print(\"\\nProcessing file\")\n",
|
530 |
" result = await asyncio.gather(processed_file)\n",
|
531 |
"\n",
|
532 |
-
"
|
533 |
" print(result)\n",
|
534 |
"\n",
|
535 |
" output_file = open(CATEGORY_REFERENCE_OUTPUT_FILE, \"r+\")\n",
|
@@ -537,7 +537,8 @@
|
|
537 |
"\n",
|
538 |
"\n",
|
539 |
"result = await apply_categorization()\n",
|
540 |
-
"print(result)\n"
|
|
|
541 |
]
|
542 |
}
|
543 |
],
|
|
|
529 |
" print(\"\\nProcessing file\")\n",
|
530 |
" result = await asyncio.gather(processed_file)\n",
|
531 |
"\n",
|
532 |
+
" save_results(result)\n",
|
533 |
" print(result)\n",
|
534 |
"\n",
|
535 |
" output_file = open(CATEGORY_REFERENCE_OUTPUT_FILE, \"r+\")\n",
|
|
|
537 |
"\n",
|
538 |
"\n",
|
539 |
"result = await apply_categorization()\n",
|
540 |
+
"print(result)\n",
|
541 |
+
"\n"
|
542 |
]
|
543 |
}
|
544 |
],
|
config/index.py
DELETED
@@ -1,34 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
from dotenv import load_dotenv
|
3 |
-
|
4 |
-
load_dotenv()
|
5 |
-
|
6 |
-
|
7 |
-
class Config:
|
8 |
-
DEBUG = os.getenv("DEBUG", False)
|
9 |
-
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI")
|
10 |
-
SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS", False)
|
11 |
-
ENVIRONMENT = os.getenv("ENVIRONMENT", "dev")
|
12 |
-
APP_HOST = os.getenv("APP_HOST", "0.0.0.0")
|
13 |
-
APP_PORT = int(os.getenv("APP_PORT", "8000"))
|
14 |
-
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
|
15 |
-
PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME")
|
16 |
-
PINECONE_ENVIRONMENT = os.getenv("PINECONE_ENVIRONMENT")
|
17 |
-
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
18 |
-
OPENAI_ORGANIZATION = os.getenv("OPENAI_ORGANIZATION")
|
19 |
-
MODEL = os.getenv("MODEL", "gpt-3.5-turbo")
|
20 |
-
EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "text-embedding-ada-002")
|
21 |
-
EMBEDDING_DIM = os.getenv("EMBEDDING_DIM", "1536")
|
22 |
-
CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "1024"))
|
23 |
-
CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "20"))
|
24 |
-
MAX_TOKENS = int(os.getenv("MAX_TOKENS", "4000"))
|
25 |
-
|
26 |
-
POSTGRES_USER = os.getenv("POSTGRES_USER")
|
27 |
-
POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD")
|
28 |
-
POSTGRES_DB_NAME = os.getenv("POSTGRES_DB_NAME")
|
29 |
-
POSTGRES_DB_HOST = os.getenv("POSTGRES_DB_HOST")
|
30 |
-
POSTGRES_DB_PORT = int(os.getenv("POSTGRES_DB_PORT", 5432))
|
31 |
-
SQLALCHEMY_DATABASE_URL = f"postgresql+asyncpg://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_DB_HOST}:{POSTGRES_DB_PORT}/{POSTGRES_DB_NAME}"
|
32 |
-
|
33 |
-
|
34 |
-
config = Config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data/.gitkeep
DELETED
File without changes
|
data/tx_data/input/transactions_2024.csv
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
2023-12-30,Comcast Internet,Expense,9.96
|
2 |
-
2023-12-30,Lemonade Home Insurance,Expense,17.53
|
3 |
-
2023-12-30,Monthly Appartment Rent,Expense,2000.0
|
4 |
-
2023-12-30,Staples Office Supplies,Expense,12.46
|
5 |
-
2023-12-29,Selling Paintings,Income,13.63
|
6 |
-
2023-12-29,Spotify,Expense,12.19
|
7 |
-
2023-12-23,Target,Expense,27.08
|
8 |
-
2023-12-22,IT Consulting,Income,541.57
|
9 |
-
2023-12-22,Phone,Expense,10.7
|
10 |
-
2023-12-20,ML Consulting,Income,2641.93
|
11 |
-
2023-12-19,Chipotle,Expense,18.9
|
12 |
-
2023-12-18,Cold Brew Coffee,Expense,17.67
|
13 |
-
2023-12-18,Gas,Expense,8.80
|
14 |
-
2023-12-18,CA Property Tax,Expense,1670.34
|
15 |
-
2022-11-26,Salary,Income,4000.36
|
16 |
-
2022-11-26,Cellphone,Expense,19.27
|
17 |
-
2022-11-26,Gym Membership,Expense,24.71
|
18 |
-
2022-11-25,Wholefoods,Expense,17.35
|
19 |
-
2022-11-24,Freelancing,Income,2409.55
|
20 |
-
2022-11-19,Spotify,Expense,20.76
|
21 |
-
2022-10-25,Blogging,Income,4044.27
|
22 |
-
2022-10-24,Uber Taxi,Expense,18.9
|
23 |
-
2022-10-23,Uber Taxi,Expense,27.54
|
24 |
-
2022-10-22,Apple Services,Expense,41.25
|
25 |
-
2022-10-21,Netflix,Expense,22.8
|
26 |
-
2022-01-16,Amazon Lux,Expense,24.11
|
27 |
-
2022-01-15,Burger King,Expense,30.08
|
28 |
-
2022-01-14,Amazon,Expense,11.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
CHANGED
@@ -1,74 +1,51 @@
|
|
|
|
|
|
|
|
|
|
1 |
import logging
|
2 |
import os
|
3 |
import uvicorn
|
4 |
-
from contextlib import asynccontextmanager
|
5 |
from fastapi import FastAPI
|
6 |
from fastapi.middleware.cors import CORSMiddleware
|
7 |
from fastapi.responses import RedirectResponse
|
8 |
from app.api.routers.chat import chat_router
|
9 |
-
from app.api.routers.user import user_router
|
10 |
-
from app.api.routers.transaction import transaction_router
|
11 |
-
from app.api.routers.file_upload import file_upload_router
|
12 |
-
from app.api.routers.income_statement import income_statement_router
|
13 |
from app.settings import init_settings
|
|
|
14 |
from fastapi.staticfiles import StaticFiles
|
15 |
-
from alembic.config import Config
|
16 |
-
from alembic import command
|
17 |
-
|
18 |
|
19 |
-
from app.engine.postgresdb import postgresdb
|
20 |
-
from config.index import config as env
|
21 |
-
|
22 |
-
logger = logging.getLogger("uvicorn")
|
23 |
|
|
|
24 |
|
25 |
init_settings()
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
def init_app(init_db: bool = True) -> FastAPI:
|
30 |
-
lifespan = None
|
31 |
|
32 |
-
|
33 |
-
postgresdb.init(env.SQLALCHEMY_DATABASE_URL, {"echo": True, "future": True})
|
34 |
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
|
|
|
|
|
|
|
|
|
|
40 |
|
41 |
-
|
|
|
|
|
|
|
42 |
|
43 |
-
if env.ENVIRONMENT == "dev":
|
44 |
-
logger.warning("Running in development mode - allowing CORS for all origins")
|
45 |
-
app.add_middleware(
|
46 |
-
CORSMiddleware,
|
47 |
-
allow_origins=["*"],
|
48 |
-
allow_credentials=True,
|
49 |
-
allow_methods=["*"],
|
50 |
-
allow_headers=["*"],
|
51 |
-
)
|
52 |
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
app.include_router(user_router)
|
57 |
-
app.include_router(transaction_router)
|
58 |
-
app.include_router(file_upload_router)
|
59 |
-
app.include_router(income_statement_router)
|
60 |
-
|
61 |
-
return app
|
62 |
-
|
63 |
-
|
64 |
-
app = init_app()
|
65 |
-
|
66 |
-
|
67 |
-
# Redirect to documentation page when accessing base URL
|
68 |
-
@app.get("/")
|
69 |
-
async def redirect_to_docs():
|
70 |
-
return RedirectResponse(url="/docs")
|
71 |
|
72 |
|
73 |
if __name__ == "__main__":
|
74 |
-
|
|
|
|
|
|
|
|
|
|
1 |
+
from dotenv import load_dotenv
|
2 |
+
|
3 |
+
load_dotenv()
|
4 |
+
|
5 |
import logging
|
6 |
import os
|
7 |
import uvicorn
|
|
|
8 |
from fastapi import FastAPI
|
9 |
from fastapi.middleware.cors import CORSMiddleware
|
10 |
from fastapi.responses import RedirectResponse
|
11 |
from app.api.routers.chat import chat_router
|
|
|
|
|
|
|
|
|
12 |
from app.settings import init_settings
|
13 |
+
from app.observability import init_observability
|
14 |
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
|
|
15 |
|
|
|
|
|
|
|
|
|
16 |
|
17 |
+
app = FastAPI()
|
18 |
|
19 |
init_settings()
|
20 |
+
init_observability()
|
|
|
|
|
|
|
|
|
21 |
|
22 |
+
environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set
|
|
|
23 |
|
24 |
+
if environment == "dev":
|
25 |
+
logger = logging.getLogger("uvicorn")
|
26 |
+
logger.warning("Running in development mode - allowing CORS for all origins")
|
27 |
+
app.add_middleware(
|
28 |
+
CORSMiddleware,
|
29 |
+
allow_origins=["*"],
|
30 |
+
allow_credentials=True,
|
31 |
+
allow_methods=["*"],
|
32 |
+
allow_headers=["*"],
|
33 |
+
)
|
34 |
|
35 |
+
# Redirect to documentation page when accessing base URL
|
36 |
+
@app.get("/")
|
37 |
+
async def redirect_to_docs():
|
38 |
+
return RedirectResponse(url="/docs")
|
39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
|
41 |
+
if os.path.exists("data"):
|
42 |
+
app.mount("/api/data", StaticFiles(directory="data"), name="static")
|
43 |
+
app.include_router(chat_router, prefix="/api/chat")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
|
46 |
if __name__ == "__main__":
|
47 |
+
app_host = os.getenv("APP_HOST", "0.0.0.0")
|
48 |
+
app_port = int(os.getenv("APP_PORT", "8000"))
|
49 |
+
reload = True if environment == "dev" else False
|
50 |
+
|
51 |
+
uvicorn.run(app="main:app", host=app_host, port=app_port, reload=reload)
|
migration/README
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
Generic single-database configuration.
|
|
|
|
migration/env.py
DELETED
@@ -1,91 +0,0 @@
|
|
1 |
-
import asyncio
|
2 |
-
from logging.config import fileConfig
|
3 |
-
|
4 |
-
from app.engine.postgresdb import Base
|
5 |
-
from sqlalchemy import Connection
|
6 |
-
from sqlalchemy import pool
|
7 |
-
from sqlalchemy.ext.asyncio import async_engine_from_config
|
8 |
-
|
9 |
-
from config.index import config as env
|
10 |
-
|
11 |
-
from alembic import context
|
12 |
-
|
13 |
-
# this is the Alembic Config object, which provides
|
14 |
-
# access to the values within the .ini file in use.
|
15 |
-
config = context.config
|
16 |
-
|
17 |
-
config.set_main_option("sqlalchemy.url", env.SQLALCHEMY_DATABASE_URL)
|
18 |
-
|
19 |
-
# Interpret the config file for Python logging.
|
20 |
-
# This line sets up loggers basically.
|
21 |
-
if config.config_file_name is not None:
|
22 |
-
fileConfig(config.config_file_name)
|
23 |
-
|
24 |
-
# Import all models so they're registered with SQLAlchemy.
|
25 |
-
import app.model.user
|
26 |
-
import app.model.transaction
|
27 |
-
import app.model.income_statement
|
28 |
-
|
29 |
-
# add your model's MetaData object here
|
30 |
-
# for 'autogenerate' support
|
31 |
-
# from myapp import mymodel
|
32 |
-
# target_metadata = mymodel.Base.metadata
|
33 |
-
target_metadata = Base.metadata
|
34 |
-
|
35 |
-
# other values from the config, defined by the needs of env.py,
|
36 |
-
# can be acquired:
|
37 |
-
# my_important_option = config.get_main_option("my_important_option")
|
38 |
-
# ... etc.
|
39 |
-
|
40 |
-
|
41 |
-
def run_migrations_offline():
|
42 |
-
"""Run migrations in 'offline' mode.
|
43 |
-
This configures the context with just a URL
|
44 |
-
and not an Engine, though an Engine is acceptable
|
45 |
-
here as well. By skipping the Engine creation
|
46 |
-
we don't even need a DBAPI to be available.
|
47 |
-
Calls to context.execute() here emit the given string to the
|
48 |
-
script output.
|
49 |
-
"""
|
50 |
-
url = config.get_main_option("sqlalchemy.url")
|
51 |
-
context.configure(
|
52 |
-
url=url,
|
53 |
-
target_metadata=target_metadata,
|
54 |
-
literal_binds=True,
|
55 |
-
dialect_opts={"paramstyle": "named"},
|
56 |
-
)
|
57 |
-
|
58 |
-
with context.begin_transaction():
|
59 |
-
context.run_migrations()
|
60 |
-
|
61 |
-
|
62 |
-
def do_run_migrations(connection: Connection) -> None:
|
63 |
-
context.configure(connection=connection, target_metadata=target_metadata)
|
64 |
-
|
65 |
-
with context.begin_transaction():
|
66 |
-
context.run_migrations()
|
67 |
-
|
68 |
-
|
69 |
-
async def run_migrations_online():
|
70 |
-
"""Run migrations in 'online' mode.
|
71 |
-
In this scenario we need to create an Engine
|
72 |
-
and associate a connection with the context.
|
73 |
-
"""
|
74 |
-
configuration = config.get_section(config.config_ini_section)
|
75 |
-
configuration["sqlalchemy.url"] = env.SQLALCHEMY_DATABASE_URL
|
76 |
-
connectable = async_engine_from_config(
|
77 |
-
configuration,
|
78 |
-
prefix="sqlalchemy.",
|
79 |
-
poolclass=pool.NullPool,
|
80 |
-
)
|
81 |
-
|
82 |
-
async with connectable.connect() as connection:
|
83 |
-
await connection.run_sync(do_run_migrations)
|
84 |
-
|
85 |
-
await connectable.dispose()
|
86 |
-
|
87 |
-
|
88 |
-
if context.is_offline_mode():
|
89 |
-
run_migrations_offline()
|
90 |
-
else:
|
91 |
-
asyncio.run(run_migrations_online())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migration/script.py.mako
DELETED
@@ -1,26 +0,0 @@
|
|
1 |
-
"""${message}
|
2 |
-
|
3 |
-
Revision ID: ${up_revision}
|
4 |
-
Revises: ${down_revision | comma,n}
|
5 |
-
Create Date: ${create_date}
|
6 |
-
|
7 |
-
"""
|
8 |
-
from typing import Sequence, Union
|
9 |
-
|
10 |
-
from alembic import op
|
11 |
-
import sqlalchemy as sa
|
12 |
-
${imports if imports else ""}
|
13 |
-
|
14 |
-
# revision identifiers, used by Alembic.
|
15 |
-
revision: str = ${repr(up_revision)}
|
16 |
-
down_revision: Union[str, None] = ${repr(down_revision)}
|
17 |
-
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
18 |
-
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
19 |
-
|
20 |
-
|
21 |
-
def upgrade() -> None:
|
22 |
-
${upgrades if upgrades else "pass"}
|
23 |
-
|
24 |
-
|
25 |
-
def downgrade() -> None:
|
26 |
-
${downgrades if downgrades else "pass"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migration/versions/4e76691ab103_add_income_statement_table.py
DELETED
@@ -1,43 +0,0 @@
|
|
1 |
-
"""add_income_statement_table
|
2 |
-
|
3 |
-
Revision ID: 4e76691ab103
|
4 |
-
Revises: 7ea44cbc5b1f
|
5 |
-
Create Date: 2024-06-03 23:59:03.717778
|
6 |
-
|
7 |
-
"""
|
8 |
-
from typing import Sequence, Union
|
9 |
-
|
10 |
-
from alembic import op
|
11 |
-
import sqlalchemy as sa
|
12 |
-
from sqlalchemy.dialects import postgresql
|
13 |
-
|
14 |
-
# revision identifiers, used by Alembic.
|
15 |
-
revision: str = '4e76691ab103'
|
16 |
-
down_revision: Union[str, None] = '7ea44cbc5b1f'
|
17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
19 |
-
|
20 |
-
|
21 |
-
def upgrade() -> None:
|
22 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
23 |
-
op.create_table('income_statement',
|
24 |
-
sa.Column('income', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
25 |
-
sa.Column('expenses', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
26 |
-
sa.Column('date_from', sa.DateTime(), nullable=False),
|
27 |
-
sa.Column('date_to', sa.DateTime(), nullable=False),
|
28 |
-
sa.Column('user_id', sa.Integer(), nullable=False),
|
29 |
-
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
30 |
-
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
31 |
-
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
32 |
-
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
33 |
-
sa.PrimaryKeyConstraint('id')
|
34 |
-
)
|
35 |
-
op.create_index(op.f('ix_income_statement_id'), 'income_statement', ['id'], unique=False)
|
36 |
-
# ### end Alembic commands ###
|
37 |
-
|
38 |
-
|
39 |
-
def downgrade() -> None:
|
40 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
41 |
-
op.drop_index(op.f('ix_income_statement_id'), table_name='income_statement')
|
42 |
-
op.drop_table('income_statement')
|
43 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migration/versions/7ea44cbc5b1f_default_timezone.py
DELETED
@@ -1,69 +0,0 @@
|
|
1 |
-
"""default_timezone
|
2 |
-
|
3 |
-
Revision ID: 7ea44cbc5b1f
|
4 |
-
Revises: 8feaedca36f9
|
5 |
-
Create Date: 2024-06-02 14:36:01.552518
|
6 |
-
|
7 |
-
"""
|
8 |
-
|
9 |
-
from typing import Sequence, Union
|
10 |
-
|
11 |
-
from alembic import op
|
12 |
-
import sqlalchemy as sa
|
13 |
-
from sqlalchemy.dialects import postgresql
|
14 |
-
|
15 |
-
# revision identifiers, used by Alembic.
|
16 |
-
revision: str = "7ea44cbc5b1f"
|
17 |
-
down_revision: Union[str, None] = "8feaedca36f9"
|
18 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
19 |
-
depends_on: Union[str, Sequence[str], None] = None
|
20 |
-
|
21 |
-
|
22 |
-
def upgrade() -> None:
|
23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
24 |
-
op.alter_column(
|
25 |
-
"transactions",
|
26 |
-
"created_at",
|
27 |
-
existing_type=postgresql.TIMESTAMP(),
|
28 |
-
type_=sa.DateTime(timezone=True),
|
29 |
-
nullable=False,
|
30 |
-
)
|
31 |
-
op.alter_column(
|
32 |
-
"transactions",
|
33 |
-
"updated_at",
|
34 |
-
existing_type=postgresql.TIMESTAMP(),
|
35 |
-
type_=sa.DateTime(timezone=True),
|
36 |
-
nullable=False,
|
37 |
-
)
|
38 |
-
op.alter_column(
|
39 |
-
"users", "created_at", existing_type=postgresql.TIMESTAMP(), type_=sa.DateTime(timezone=True), nullable=False
|
40 |
-
)
|
41 |
-
op.alter_column(
|
42 |
-
"users", "updated_at", existing_type=postgresql.TIMESTAMP(), type_=sa.DateTime(timezone=True), nullable=False
|
43 |
-
)
|
44 |
-
# ### end Alembic commands ###
|
45 |
-
|
46 |
-
|
47 |
-
def downgrade() -> None:
|
48 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
49 |
-
op.alter_column(
|
50 |
-
"users", "updated_at", existing_type=sa.DateTime(timezone=True), type_=postgresql.TIMESTAMP(), nullable=True
|
51 |
-
)
|
52 |
-
op.alter_column(
|
53 |
-
"users", "created_at", existing_type=sa.DateTime(timezone=True), type_=postgresql.TIMESTAMP(), nullable=True
|
54 |
-
)
|
55 |
-
op.alter_column(
|
56 |
-
"transactions",
|
57 |
-
"updated_at",
|
58 |
-
existing_type=sa.DateTime(timezone=True),
|
59 |
-
type_=postgresql.TIMESTAMP(),
|
60 |
-
nullable=True,
|
61 |
-
)
|
62 |
-
op.alter_column(
|
63 |
-
"transactions",
|
64 |
-
"created_at",
|
65 |
-
existing_type=sa.DateTime(timezone=True),
|
66 |
-
type_=postgresql.TIMESTAMP(),
|
67 |
-
nullable=True,
|
68 |
-
)
|
69 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migration/versions/8feaedca36f9_users_datatype_updates.py
DELETED
@@ -1,74 +0,0 @@
|
|
1 |
-
"""Remove_Transactions_From_Users
|
2 |
-
|
3 |
-
Revision ID: 8feaedca36f9
|
4 |
-
Revises: cd515c44401d
|
5 |
-
Create Date: 2024-06-02 01:26:54.731002
|
6 |
-
|
7 |
-
"""
|
8 |
-
from typing import Sequence, Union
|
9 |
-
|
10 |
-
from alembic import op
|
11 |
-
import sqlalchemy as sa
|
12 |
-
from sqlalchemy.dialects import postgresql
|
13 |
-
|
14 |
-
# revision identifiers, used by Alembic.
|
15 |
-
revision: str = '8feaedca36f9'
|
16 |
-
down_revision: Union[str, None] = 'cd515c44401d'
|
17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
19 |
-
|
20 |
-
|
21 |
-
def upgrade() -> None:
|
22 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
23 |
-
op.alter_column('transactions', 'transaction_date',
|
24 |
-
existing_type=postgresql.TIMESTAMP(),
|
25 |
-
nullable=False)
|
26 |
-
op.alter_column('transactions', 'category',
|
27 |
-
existing_type=sa.VARCHAR(),
|
28 |
-
nullable=False)
|
29 |
-
op.alter_column('transactions', 'name_description',
|
30 |
-
existing_type=sa.VARCHAR(),
|
31 |
-
nullable=False)
|
32 |
-
op.alter_column('transactions', 'amount',
|
33 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
34 |
-
nullable=False)
|
35 |
-
op.alter_column('transactions', 'type',
|
36 |
-
existing_type=sa.VARCHAR(),
|
37 |
-
nullable=False)
|
38 |
-
op.add_column('users', sa.Column('hashed_password', sa.String(), nullable=False))
|
39 |
-
op.add_column('users', sa.Column('is_deleted', sa.Boolean(), nullable=False))
|
40 |
-
op.alter_column('users', 'name',
|
41 |
-
existing_type=sa.VARCHAR(),
|
42 |
-
nullable=False)
|
43 |
-
op.alter_column('users', 'email',
|
44 |
-
existing_type=sa.VARCHAR(),
|
45 |
-
nullable=False)
|
46 |
-
# ### end Alembic commands ###
|
47 |
-
|
48 |
-
|
49 |
-
def downgrade() -> None:
|
50 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
51 |
-
op.alter_column('users', 'email',
|
52 |
-
existing_type=sa.VARCHAR(),
|
53 |
-
nullable=True)
|
54 |
-
op.alter_column('users', 'name',
|
55 |
-
existing_type=sa.VARCHAR(),
|
56 |
-
nullable=True)
|
57 |
-
op.drop_column('users', 'is_deleted')
|
58 |
-
op.drop_column('users', 'hashed_password')
|
59 |
-
op.alter_column('transactions', 'type',
|
60 |
-
existing_type=sa.VARCHAR(),
|
61 |
-
nullable=True)
|
62 |
-
op.alter_column('transactions', 'amount',
|
63 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
64 |
-
nullable=True)
|
65 |
-
op.alter_column('transactions', 'name_description',
|
66 |
-
existing_type=sa.VARCHAR(),
|
67 |
-
nullable=True)
|
68 |
-
op.alter_column('transactions', 'category',
|
69 |
-
existing_type=sa.VARCHAR(),
|
70 |
-
nullable=True)
|
71 |
-
op.alter_column('transactions', 'transaction_date',
|
72 |
-
existing_type=postgresql.TIMESTAMP(),
|
73 |
-
nullable=True)
|
74 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migration/versions/cd515c44401d_add_users_transactions_tables.py
DELETED
@@ -1,55 +0,0 @@
|
|
1 |
-
"""Add users, transactions tables
|
2 |
-
|
3 |
-
Revision ID: cd515c44401d
|
4 |
-
Revises:
|
5 |
-
Create Date: 2024-06-01 01:32:42.500524
|
6 |
-
|
7 |
-
"""
|
8 |
-
from typing import Sequence, Union
|
9 |
-
|
10 |
-
from alembic import op
|
11 |
-
import sqlalchemy as sa
|
12 |
-
|
13 |
-
|
14 |
-
# revision identifiers, used by Alembic.
|
15 |
-
revision: str = 'cd515c44401d'
|
16 |
-
down_revision: Union[str, None] = None
|
17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
19 |
-
|
20 |
-
|
21 |
-
def upgrade() -> None:
|
22 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
23 |
-
op.create_table('users',
|
24 |
-
sa.Column('name', sa.String(), nullable=True),
|
25 |
-
sa.Column('email', sa.String(), nullable=True),
|
26 |
-
sa.Column('id', sa.Integer(), nullable=False),
|
27 |
-
sa.Column('created_at', sa.DateTime(), nullable=True),
|
28 |
-
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
29 |
-
sa.PrimaryKeyConstraint('id')
|
30 |
-
)
|
31 |
-
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
32 |
-
op.create_table('transactions',
|
33 |
-
sa.Column('transaction_date', sa.DateTime(), nullable=True),
|
34 |
-
sa.Column('category', sa.String(), nullable=True),
|
35 |
-
sa.Column('name_description', sa.String(), nullable=True),
|
36 |
-
sa.Column('amount', sa.Float(), nullable=True),
|
37 |
-
sa.Column('type', sa.String(), nullable=True),
|
38 |
-
sa.Column('user_id', sa.Integer(), nullable=True),
|
39 |
-
sa.Column('id', sa.Integer(), nullable=False),
|
40 |
-
sa.Column('created_at', sa.DateTime(), nullable=True),
|
41 |
-
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
42 |
-
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
43 |
-
sa.PrimaryKeyConstraint('id')
|
44 |
-
)
|
45 |
-
op.create_index(op.f('ix_transactions_id'), 'transactions', ['id'], unique=False)
|
46 |
-
# ### end Alembic commands ###
|
47 |
-
|
48 |
-
|
49 |
-
def downgrade() -> None:
|
50 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
51 |
-
op.drop_index(op.f('ix_transactions_id'), table_name='transactions')
|
52 |
-
op.drop_table('transactions')
|
53 |
-
op.drop_index(op.f('ix_users_id'), table_name='users')
|
54 |
-
op.drop_table('users')
|
55 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
poetry.lock
CHANGED
@@ -123,25 +123,6 @@ files = [
|
|
123 |
[package.dependencies]
|
124 |
typing-extensions = "*"
|
125 |
|
126 |
-
[[package]]
|
127 |
-
name = "alembic"
|
128 |
-
version = "1.13.1"
|
129 |
-
description = "A database migration tool for SQLAlchemy."
|
130 |
-
optional = false
|
131 |
-
python-versions = ">=3.8"
|
132 |
-
files = [
|
133 |
-
{file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"},
|
134 |
-
{file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"},
|
135 |
-
]
|
136 |
-
|
137 |
-
[package.dependencies]
|
138 |
-
Mako = "*"
|
139 |
-
SQLAlchemy = ">=1.3.0"
|
140 |
-
typing-extensions = ">=4"
|
141 |
-
|
142 |
-
[package.extras]
|
143 |
-
tz = ["backports.zoneinfo"]
|
144 |
-
|
145 |
[[package]]
|
146 |
name = "annotated-types"
|
147 |
version = "0.7.0"
|
@@ -198,87 +179,6 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin
|
|
198 |
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
199 |
trio = ["trio (>=0.23)"]
|
200 |
|
201 |
-
[[package]]
|
202 |
-
name = "async-timeout"
|
203 |
-
version = "4.0.3"
|
204 |
-
description = "Timeout context manager for asyncio programs"
|
205 |
-
optional = false
|
206 |
-
python-versions = ">=3.7"
|
207 |
-
files = [
|
208 |
-
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
|
209 |
-
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
|
210 |
-
]
|
211 |
-
|
212 |
-
[[package]]
|
213 |
-
name = "asyncio"
|
214 |
-
version = "3.4.3"
|
215 |
-
description = "reference implementation of PEP 3156"
|
216 |
-
optional = false
|
217 |
-
python-versions = "*"
|
218 |
-
files = [
|
219 |
-
{file = "asyncio-3.4.3-cp33-none-win32.whl", hash = "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de"},
|
220 |
-
{file = "asyncio-3.4.3-cp33-none-win_amd64.whl", hash = "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c"},
|
221 |
-
{file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"},
|
222 |
-
{file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"},
|
223 |
-
]
|
224 |
-
|
225 |
-
[[package]]
|
226 |
-
name = "asyncpg"
|
227 |
-
version = "0.29.0"
|
228 |
-
description = "An asyncio PostgreSQL driver"
|
229 |
-
optional = false
|
230 |
-
python-versions = ">=3.8.0"
|
231 |
-
files = [
|
232 |
-
{file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"},
|
233 |
-
{file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"},
|
234 |
-
{file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"},
|
235 |
-
{file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"},
|
236 |
-
{file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"},
|
237 |
-
{file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"},
|
238 |
-
{file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"},
|
239 |
-
{file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"},
|
240 |
-
{file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"},
|
241 |
-
{file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"},
|
242 |
-
{file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"},
|
243 |
-
{file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"},
|
244 |
-
{file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"},
|
245 |
-
{file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"},
|
246 |
-
{file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"},
|
247 |
-
{file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"},
|
248 |
-
{file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"},
|
249 |
-
{file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"},
|
250 |
-
{file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"},
|
251 |
-
{file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"},
|
252 |
-
{file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"},
|
253 |
-
{file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"},
|
254 |
-
{file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"},
|
255 |
-
{file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"},
|
256 |
-
{file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"},
|
257 |
-
{file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"},
|
258 |
-
{file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"},
|
259 |
-
{file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"},
|
260 |
-
{file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"},
|
261 |
-
{file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"},
|
262 |
-
{file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"},
|
263 |
-
{file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"},
|
264 |
-
{file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"},
|
265 |
-
{file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"},
|
266 |
-
{file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"},
|
267 |
-
{file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"},
|
268 |
-
{file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"},
|
269 |
-
{file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"},
|
270 |
-
{file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"},
|
271 |
-
{file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"},
|
272 |
-
{file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"},
|
273 |
-
]
|
274 |
-
|
275 |
-
[package.dependencies]
|
276 |
-
async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""}
|
277 |
-
|
278 |
-
[package.extras]
|
279 |
-
docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
280 |
-
test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"]
|
281 |
-
|
282 |
[[package]]
|
283 |
name = "attrs"
|
284 |
version = "23.2.0"
|
@@ -541,20 +441,6 @@ files = [
|
|
541 |
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
|
542 |
]
|
543 |
|
544 |
-
[[package]]
|
545 |
-
name = "execnet"
|
546 |
-
version = "2.1.1"
|
547 |
-
description = "execnet: rapid multi-Python deployment"
|
548 |
-
optional = false
|
549 |
-
python-versions = ">=3.8"
|
550 |
-
files = [
|
551 |
-
{file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"},
|
552 |
-
{file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"},
|
553 |
-
]
|
554 |
-
|
555 |
-
[package.extras]
|
556 |
-
testing = ["hatch", "pre-commit", "pytest", "tox"]
|
557 |
-
|
558 |
[[package]]
|
559 |
name = "fastapi"
|
560 |
version = "0.109.2"
|
@@ -1039,17 +925,6 @@ files = [
|
|
1039 |
{file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"},
|
1040 |
]
|
1041 |
|
1042 |
-
[[package]]
|
1043 |
-
name = "iniconfig"
|
1044 |
-
version = "2.0.0"
|
1045 |
-
description = "brain-dead simple config-ini parsing"
|
1046 |
-
optional = false
|
1047 |
-
python-versions = ">=3.7"
|
1048 |
-
files = [
|
1049 |
-
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
1050 |
-
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
1051 |
-
]
|
1052 |
-
|
1053 |
[[package]]
|
1054 |
name = "jinja2"
|
1055 |
version = "3.1.4"
|
@@ -1590,25 +1465,6 @@ files = [
|
|
1590 |
httpx = ">=0.20.0"
|
1591 |
pydantic = ">=1.10"
|
1592 |
|
1593 |
-
[[package]]
|
1594 |
-
name = "mako"
|
1595 |
-
version = "1.3.5"
|
1596 |
-
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
|
1597 |
-
optional = false
|
1598 |
-
python-versions = ">=3.8"
|
1599 |
-
files = [
|
1600 |
-
{file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"},
|
1601 |
-
{file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"},
|
1602 |
-
]
|
1603 |
-
|
1604 |
-
[package.dependencies]
|
1605 |
-
MarkupSafe = ">=0.9.2"
|
1606 |
-
|
1607 |
-
[package.extras]
|
1608 |
-
babel = ["Babel"]
|
1609 |
-
lingua = ["lingua"]
|
1610 |
-
testing = ["pytest"]
|
1611 |
-
|
1612 |
[[package]]
|
1613 |
name = "markupsafe"
|
1614 |
version = "2.1.5"
|
@@ -1697,20 +1553,6 @@ dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
|
|
1697 |
docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"]
|
1698 |
tests = ["pytest", "pytz", "simplejson"]
|
1699 |
|
1700 |
-
[[package]]
|
1701 |
-
name = "mirakuru"
|
1702 |
-
version = "2.5.2"
|
1703 |
-
description = "Process executor (not only) for tests."
|
1704 |
-
optional = false
|
1705 |
-
python-versions = ">=3.8"
|
1706 |
-
files = [
|
1707 |
-
{file = "mirakuru-2.5.2-py3-none-any.whl", hash = "sha256:90c2d90a8cf14349b2f33e6db30a16acd855499811e0312e56cf80ceacf2d3e5"},
|
1708 |
-
{file = "mirakuru-2.5.2.tar.gz", hash = "sha256:41ca583d355eb7a6cfdc21c1aea549979d685c27b57239b88725434f115a7132"},
|
1709 |
-
]
|
1710 |
-
|
1711 |
-
[package.dependencies]
|
1712 |
-
psutil = {version = ">=4.0.0", markers = "sys_platform != \"cygwin\""}
|
1713 |
-
|
1714 |
[[package]]
|
1715 |
name = "monotonic"
|
1716 |
version = "1.6"
|
@@ -2698,32 +2540,6 @@ urllib3 = {version = ">=1.26.0", markers = "python_version >= \"3.8\" and python
|
|
2698 |
[package.extras]
|
2699 |
grpc = ["googleapis-common-protos (>=1.53.0)", "grpc-gateway-protoc-gen-openapiv2 (==0.1.0)", "grpcio (>=1.44.0)", "grpcio (>=1.59.0)", "lz4 (>=3.1.3)", "protobuf (>=3.20.0,<3.21.0)"]
|
2700 |
|
2701 |
-
[[package]]
|
2702 |
-
name = "pluggy"
|
2703 |
-
version = "1.5.0"
|
2704 |
-
description = "plugin and hook calling mechanisms for python"
|
2705 |
-
optional = false
|
2706 |
-
python-versions = ">=3.8"
|
2707 |
-
files = [
|
2708 |
-
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
2709 |
-
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
2710 |
-
]
|
2711 |
-
|
2712 |
-
[package.extras]
|
2713 |
-
dev = ["pre-commit", "tox"]
|
2714 |
-
testing = ["pytest", "pytest-benchmark"]
|
2715 |
-
|
2716 |
-
[[package]]
|
2717 |
-
name = "port-for"
|
2718 |
-
version = "0.7.2"
|
2719 |
-
description = "Utility that helps with local TCP ports management. It can find an unused TCP localhost port and remember the association."
|
2720 |
-
optional = false
|
2721 |
-
python-versions = ">=3.8"
|
2722 |
-
files = [
|
2723 |
-
{file = "port-for-0.7.2.tar.gz", hash = "sha256:074f29335130578aa42fef3726985e57d01c15189e509633a8a1b0b7f9226349"},
|
2724 |
-
{file = "port_for-0.7.2-py3-none-any.whl", hash = "sha256:16b279ab4f210bad33515c45bd9af0c6e048ab24c3b6bbd9cfc7e451782617df"},
|
2725 |
-
]
|
2726 |
-
|
2727 |
[[package]]
|
2728 |
name = "posthog"
|
2729 |
version = "3.5.0"
|
@@ -2767,129 +2583,6 @@ files = [
|
|
2767 |
{file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"},
|
2768 |
]
|
2769 |
|
2770 |
-
[[package]]
|
2771 |
-
name = "psutil"
|
2772 |
-
version = "5.9.8"
|
2773 |
-
description = "Cross-platform lib for process and system monitoring in Python."
|
2774 |
-
optional = false
|
2775 |
-
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
2776 |
-
files = [
|
2777 |
-
{file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"},
|
2778 |
-
{file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"},
|
2779 |
-
{file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"},
|
2780 |
-
{file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"},
|
2781 |
-
{file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"},
|
2782 |
-
{file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"},
|
2783 |
-
{file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"},
|
2784 |
-
{file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"},
|
2785 |
-
{file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"},
|
2786 |
-
{file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"},
|
2787 |
-
{file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"},
|
2788 |
-
{file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"},
|
2789 |
-
{file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"},
|
2790 |
-
{file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"},
|
2791 |
-
{file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"},
|
2792 |
-
{file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"},
|
2793 |
-
]
|
2794 |
-
|
2795 |
-
[package.extras]
|
2796 |
-
test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
|
2797 |
-
|
2798 |
-
[[package]]
|
2799 |
-
name = "psycopg"
|
2800 |
-
version = "3.1.19"
|
2801 |
-
description = "PostgreSQL database adapter for Python"
|
2802 |
-
optional = false
|
2803 |
-
python-versions = ">=3.7"
|
2804 |
-
files = [
|
2805 |
-
{file = "psycopg-3.1.19-py3-none-any.whl", hash = "sha256:dca5e5521c859f6606686432ae1c94e8766d29cc91f2ee595378c510cc5b0731"},
|
2806 |
-
{file = "psycopg-3.1.19.tar.gz", hash = "sha256:92d7b78ad82426cdcf1a0440678209faa890c6e1721361c2f8901f0dccd62961"},
|
2807 |
-
]
|
2808 |
-
|
2809 |
-
[package.dependencies]
|
2810 |
-
typing-extensions = ">=4.1"
|
2811 |
-
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
2812 |
-
|
2813 |
-
[package.extras]
|
2814 |
-
binary = ["psycopg-binary (==3.1.19)"]
|
2815 |
-
c = ["psycopg-c (==3.1.19)"]
|
2816 |
-
dev = ["black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
|
2817 |
-
docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"]
|
2818 |
-
pool = ["psycopg-pool"]
|
2819 |
-
test = ["anyio (>=3.6.2,<4.0)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"]
|
2820 |
-
|
2821 |
-
[[package]]
|
2822 |
-
name = "psycopg-binary"
|
2823 |
-
version = "3.1.19"
|
2824 |
-
description = "PostgreSQL database adapter for Python -- C optimisation distribution"
|
2825 |
-
optional = false
|
2826 |
-
python-versions = ">=3.7"
|
2827 |
-
files = [
|
2828 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7204818f05151dd08f8f851defb01972ec9d2cc925608eb0de232563f203f354"},
|
2829 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4e67fd86758dbeac85641419a54f84d74495a8683b58ad5dfad08b7fc37a8f"},
|
2830 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12173e34b176e93ad2da913de30f774d5119c2d4d4640c6858d2d77dfa6c9bf"},
|
2831 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:052f5193304066318853b4b2e248f523c8f52b371fc4e95d4ef63baee3f30955"},
|
2832 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29008f3f8977f600b8a7fb07c2e041b01645b08121760609cc45e861a0364dc9"},
|
2833 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6a9a651a08d876303ed059c9553df18b3c13c3406584a70a8f37f1a1fe2709"},
|
2834 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:91a645e6468c4f064b7f4f3b81074bdd68fe5aa2b8c5107de15dcd85ba6141be"},
|
2835 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5c6956808fd5cf0576de5a602243af8e04594b25b9a28675feddc71c5526410a"},
|
2836 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:1622ca27d5a7a98f7d8f35e8b146dc7efda4a4b6241d2edf7e076bd6bcecbeb4"},
|
2837 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a100482950a55228f648bd382bb71bfaff520002f29845274fccbbf02e28bd52"},
|
2838 |
-
{file = "psycopg_binary-3.1.19-cp310-cp310-win_amd64.whl", hash = "sha256:955ca8905c0251fc4af7ce0a20999e824a25652f53a558ab548b60969f1f368e"},
|
2839 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cf49e91dcf699b8a449944ed898ef1466b39b92720613838791a551bc8f587a"},
|
2840 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:964c307e400c5f33fa762ba1e19853e048814fcfbd9679cc923431adb7a2ead2"},
|
2841 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3433924e1b14074798331dc2bfae2af452ed7888067f2fc145835704d8981b15"},
|
2842 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00879d4c6be4b3afc510073f48a5e960f797200e261ab3d9bd9b7746a08c669d"},
|
2843 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34a6997c80f86d3dd80a4f078bb3b200079c47eeda4fd409d8899b883c90d2ac"},
|
2844 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0106e42b481677c41caa69474fe530f786dcef88b11b70000f0e45a03534bc8f"},
|
2845 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81efe09ba27533e35709905c3061db4dc9fb814f637360578d065e2061fbb116"},
|
2846 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d312d6dddc18d9c164e1893706269c293cba1923118349d375962b1188dafb01"},
|
2847 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:bfd2c734da9950f7afaad5f132088e0e1478f32f042881fca6651bb0c8d14206"},
|
2848 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8a732610a5a6b4f06dadcf9288688a8ff202fd556d971436a123b7adb85596e2"},
|
2849 |
-
{file = "psycopg_binary-3.1.19-cp311-cp311-win_amd64.whl", hash = "sha256:321814a9a3ad785855a821b842aba08ca1b7de7dfb2979a2f0492dca9ec4ae70"},
|
2850 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4aa0ca13bb8a725bb6d12c13999217fd5bc8b86a12589f28a74b93e076fbb959"},
|
2851 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:469424e354ebcec949aa6aa30e5a9edc352a899d9a68ad7a48f97df83cc914cf"},
|
2852 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04f5349313529ae1f1c42fe1aa0443faaf50fdf12d13866c2cc49683bfa53d0"},
|
2853 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959feabddc7fffac89b054d6f23f3b3c62d7d3c90cd414a02e3747495597f150"},
|
2854 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e9da624a6ca4bc5f7fa1f03f8485446b5b81d5787b6beea2b4f8d9dbef878ad7"},
|
2855 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1823221a6b96e38b15686170d4fc5b36073efcb87cce7d3da660440b50077f6"},
|
2856 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:866db42f986298f0cf15d805225eb8df2228bf19f7997d7f1cb5f388cbfc6a0f"},
|
2857 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:738c34657305b5973af6dbb6711b07b179dfdd21196d60039ca30a74bafe9648"},
|
2858 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb9758473200384a04374d0e0cac6f451218ff6945a024f65a1526802c34e56e"},
|
2859 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e991632777e217953ac960726158987da684086dd813ac85038c595e7382c91"},
|
2860 |
-
{file = "psycopg_binary-3.1.19-cp312-cp312-win_amd64.whl", hash = "sha256:1d87484dd42c8783c44a30400949efb3d81ef2487eaa7d64d1c54df90cf8b97a"},
|
2861 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d1d1723d7449c12bb61aca7eb6e0c6ab2863cd8dc0019273cc4d4a1982f84bdb"},
|
2862 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e538a8671005641fa195eab962f85cf0504defbd3b548c4c8fc27102a59f687b"},
|
2863 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c50592bc8517092f40979e4a5d934f96a1737a77724bb1d121eb78b614b30fc8"},
|
2864 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95f16ae82bc242b76cd3c3e5156441e2bd85ff9ec3a9869d750aad443e46073c"},
|
2865 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebd1e98e865e9a28ce0cb2c25b7dfd752f0d1f0a423165b55cd32a431dcc0f4"},
|
2866 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:49cd7af7d49e438a39593d1dd8cab106a1912536c2b78a4d814ebdff2786094e"},
|
2867 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:affebd61aa3b7a8880fd4ac3ee94722940125ff83ff485e1a7c76be9adaabb38"},
|
2868 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d1bac282f140fa092f2bbb6c36ed82270b4a21a6fc55d4b16748ed9f55e50fdb"},
|
2869 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1285aa54449e362b1d30d92b2dc042ad3ee80f479cc4e323448d0a0a8a1641fa"},
|
2870 |
-
{file = "psycopg_binary-3.1.19-cp37-cp37m-win_amd64.whl", hash = "sha256:6cff31af8155dc9ee364098a328bab688c887c732c66b8d027e5b03818ca0287"},
|
2871 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d9b689c4a17dd3130791dcbb8c30dbf05602f7c2d56c792e193fb49adc7bf5f8"},
|
2872 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:017518bd2de4851adc826a224fb105411e148ad845e11355edd6786ba3dfedf5"},
|
2873 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c35fd811f339a3cbe7f9b54b2d9a5e592e57426c6cc1051632a62c59c4810208"},
|
2874 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38ed45ec9673709bfa5bc17f140e71dd4cca56d4e58ef7fd50d5a5043a4f55c6"},
|
2875 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:433f1c256108f9e26f480a8cd6ddb0fb37dbc87d7f5a97e4540a9da9b881f23f"},
|
2876 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ed61e43bf5dc8d0936daf03a19fef3168d64191dbe66483f7ad08c4cea0bc36b"},
|
2877 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ae8109ff9fdf1fa0cb87ab6645298693fdd2666a7f5f85660df88f6965e0bb7"},
|
2878 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a53809ee02e3952fae7977c19b30fd828bd117b8f5edf17a3a94212feb57faaf"},
|
2879 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9d39d5ffc151fb33bcd55b99b0e8957299c0b1b3e5a1a5f4399c1287ef0051a9"},
|
2880 |
-
{file = "psycopg_binary-3.1.19-cp38-cp38-win_amd64.whl", hash = "sha256:e14bc8250000921fcccd53722f86b3b3d1b57db901e206e49e2ab2afc5919c2d"},
|
2881 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd88c5cea4efe614d5004fb5f5dcdea3d7d59422be796689e779e03363102d24"},
|
2882 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:621a814e60825162d38760c66351b4df679fd422c848b7c2f86ad399bff27145"},
|
2883 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46e50c05952b59a214e27d3606f6d510aaa429daed898e16b8a37bfbacc81acc"},
|
2884 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03354a9db667c27946e70162cb0042c3929154167f3678a30d23cebfe0ad55b5"},
|
2885 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c2f3b79037581afec7baa2bdbcb0a1787f1758744a7662099b0eca2d721cb"},
|
2886 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6469ebd9e93327e9f5f36dcf8692fb1e7aeaf70087c1c15d4f2c020e0be3a891"},
|
2887 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:85bca9765c04b6be90cb46e7566ffe0faa2d7480ff5c8d5e055ac427f039fd24"},
|
2888 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:a836610d5c75e9cff98b9fdb3559c007c785c09eaa84a60d5d10ef6f85f671e8"},
|
2889 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ef8de7a1d9fb3518cc6b58e3c80b75a824209ad52b90c542686c912db8553dad"},
|
2890 |
-
{file = "psycopg_binary-3.1.19-cp39-cp39-win_amd64.whl", hash = "sha256:76fcd33342f38e35cd6b5408f1bc117d55ab8b16e5019d99b6d3ce0356c51717"},
|
2891 |
-
]
|
2892 |
-
|
2893 |
[[package]]
|
2894 |
name = "pydantic"
|
2895 |
version = "2.7.2"
|
@@ -3018,82 +2711,6 @@ docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"]
|
|
3018 |
full = ["Pillow (>=8.0.0)", "PyCryptodome", "cryptography"]
|
3019 |
image = ["Pillow (>=8.0.0)"]
|
3020 |
|
3021 |
-
[[package]]
|
3022 |
-
name = "pytest"
|
3023 |
-
version = "8.2.1"
|
3024 |
-
description = "pytest: simple powerful testing with Python"
|
3025 |
-
optional = false
|
3026 |
-
python-versions = ">=3.8"
|
3027 |
-
files = [
|
3028 |
-
{file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
|
3029 |
-
{file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
|
3030 |
-
]
|
3031 |
-
|
3032 |
-
[package.dependencies]
|
3033 |
-
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
3034 |
-
iniconfig = "*"
|
3035 |
-
packaging = "*"
|
3036 |
-
pluggy = ">=1.5,<2.0"
|
3037 |
-
|
3038 |
-
[package.extras]
|
3039 |
-
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
3040 |
-
|
3041 |
-
[[package]]
|
3042 |
-
name = "pytest-asyncio"
|
3043 |
-
version = "0.23.7"
|
3044 |
-
description = "Pytest support for asyncio"
|
3045 |
-
optional = false
|
3046 |
-
python-versions = ">=3.8"
|
3047 |
-
files = [
|
3048 |
-
{file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
|
3049 |
-
{file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
|
3050 |
-
]
|
3051 |
-
|
3052 |
-
[package.dependencies]
|
3053 |
-
pytest = ">=7.0.0,<9"
|
3054 |
-
|
3055 |
-
[package.extras]
|
3056 |
-
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
|
3057 |
-
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
3058 |
-
|
3059 |
-
[[package]]
|
3060 |
-
name = "pytest-postgresql"
|
3061 |
-
version = "6.0.0"
|
3062 |
-
description = "Postgresql fixtures and fixture factories for Pytest."
|
3063 |
-
optional = false
|
3064 |
-
python-versions = ">=3.8"
|
3065 |
-
files = [
|
3066 |
-
{file = "pytest-postgresql-6.0.0.tar.gz", hash = "sha256:6a4d8e600a2eef273f3c0e846cd0b2ea577282e252de29b4ca854bfb929bb682"},
|
3067 |
-
{file = "pytest_postgresql-6.0.0-py3-none-any.whl", hash = "sha256:f14272bffad16a74d9a63f4cc828f243a12ae92995e236b68fd53154760e6a5a"},
|
3068 |
-
]
|
3069 |
-
|
3070 |
-
[package.dependencies]
|
3071 |
-
mirakuru = "*"
|
3072 |
-
port-for = ">=0.6.0"
|
3073 |
-
psycopg = ">=3.0.0"
|
3074 |
-
pytest = ">=6.2"
|
3075 |
-
setuptools = "*"
|
3076 |
-
|
3077 |
-
[[package]]
|
3078 |
-
name = "pytest-xdist"
|
3079 |
-
version = "3.6.1"
|
3080 |
-
description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
|
3081 |
-
optional = false
|
3082 |
-
python-versions = ">=3.8"
|
3083 |
-
files = [
|
3084 |
-
{file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"},
|
3085 |
-
{file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"},
|
3086 |
-
]
|
3087 |
-
|
3088 |
-
[package.dependencies]
|
3089 |
-
execnet = ">=2.1"
|
3090 |
-
pytest = ">=7.0.0"
|
3091 |
-
|
3092 |
-
[package.extras]
|
3093 |
-
psutil = ["psutil (>=3.0)"]
|
3094 |
-
setproctitle = ["setproctitle"]
|
3095 |
-
testing = ["filelock"]
|
3096 |
-
|
3097 |
[[package]]
|
3098 |
name = "python-dateutil"
|
3099 |
version = "2.9.0.post0"
|
@@ -3122,20 +2739,6 @@ files = [
|
|
3122 |
[package.extras]
|
3123 |
cli = ["click (>=5.0)"]
|
3124 |
|
3125 |
-
[[package]]
|
3126 |
-
name = "python-multipart"
|
3127 |
-
version = "0.0.9"
|
3128 |
-
description = "A streaming multipart parser for Python"
|
3129 |
-
optional = false
|
3130 |
-
python-versions = ">=3.8"
|
3131 |
-
files = [
|
3132 |
-
{file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"},
|
3133 |
-
{file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"},
|
3134 |
-
]
|
3135 |
-
|
3136 |
-
[package.extras]
|
3137 |
-
dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"]
|
3138 |
-
|
3139 |
[[package]]
|
3140 |
name = "pytz"
|
3141 |
version = "2024.1"
|
@@ -4390,4 +3993,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more
|
|
4390 |
[metadata]
|
4391 |
lock-version = "2.0"
|
4392 |
python-versions = "^3.11,<3.12"
|
4393 |
-
content-hash = "
|
|
|
123 |
[package.dependencies]
|
124 |
typing-extensions = "*"
|
125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
[[package]]
|
127 |
name = "annotated-types"
|
128 |
version = "0.7.0"
|
|
|
179 |
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
180 |
trio = ["trio (>=0.23)"]
|
181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
[[package]]
|
183 |
name = "attrs"
|
184 |
version = "23.2.0"
|
|
|
441 |
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
|
442 |
]
|
443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
444 |
[[package]]
|
445 |
name = "fastapi"
|
446 |
version = "0.109.2"
|
|
|
925 |
{file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"},
|
926 |
]
|
927 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
928 |
[[package]]
|
929 |
name = "jinja2"
|
930 |
version = "3.1.4"
|
|
|
1465 |
httpx = ">=0.20.0"
|
1466 |
pydantic = ">=1.10"
|
1467 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1468 |
[[package]]
|
1469 |
name = "markupsafe"
|
1470 |
version = "2.1.5"
|
|
|
1553 |
docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"]
|
1554 |
tests = ["pytest", "pytz", "simplejson"]
|
1555 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1556 |
[[package]]
|
1557 |
name = "monotonic"
|
1558 |
version = "1.6"
|
|
|
2540 |
[package.extras]
|
2541 |
grpc = ["googleapis-common-protos (>=1.53.0)", "grpc-gateway-protoc-gen-openapiv2 (==0.1.0)", "grpcio (>=1.44.0)", "grpcio (>=1.59.0)", "lz4 (>=3.1.3)", "protobuf (>=3.20.0,<3.21.0)"]
|
2542 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2543 |
[[package]]
|
2544 |
name = "posthog"
|
2545 |
version = "3.5.0"
|
|
|
2583 |
{file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"},
|
2584 |
]
|
2585 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2586 |
[[package]]
|
2587 |
name = "pydantic"
|
2588 |
version = "2.7.2"
|
|
|
2711 |
full = ["Pillow (>=8.0.0)", "PyCryptodome", "cryptography"]
|
2712 |
image = ["Pillow (>=8.0.0)"]
|
2713 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2714 |
[[package]]
|
2715 |
name = "python-dateutil"
|
2716 |
version = "2.9.0.post0"
|
|
|
2739 |
[package.extras]
|
2740 |
cli = ["click (>=5.0)"]
|
2741 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2742 |
[[package]]
|
2743 |
name = "pytz"
|
2744 |
version = "2024.1"
|
|
|
3993 |
[metadata]
|
3994 |
lock-version = "2.0"
|
3995 |
python-versions = "^3.11,<3.12"
|
3996 |
+
content-hash = "764a6a2edfd938e4128d185bb383ddc23dadaa25b9f5cf7215998a90b3cc760e"
|
pyproject.toml
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
name = "app"
|
4 |
version = "0.1.0"
|
5 |
description = ""
|
6 |
-
authors = [ "
|
7 |
readme = "README.md"
|
8 |
package-mode = false
|
9 |
|
@@ -28,33 +28,14 @@ pydantic = "^2.7.2"
|
|
28 |
dateparser = "^1.2.0"
|
29 |
pandas = "^2.2.2"
|
30 |
path = "^16.14.0"
|
31 |
-
pytest-xdist = "^3.6.1"
|
32 |
-
pytest-asyncio = "^0.23.7"
|
33 |
-
httpx = "^0.27.0"
|
34 |
-
alembic = "^1.13.1"
|
35 |
-
asyncpg = "^0.29.0"
|
36 |
-
asyncio = "^3.4.3"
|
37 |
-
pytest-postgresql = "^6.0.0"
|
38 |
-
psycopg-binary = "^3.1.19"
|
39 |
-
python-multipart = "^0.0.9"
|
40 |
|
41 |
[tool.poetry.dependencies.uvicorn]
|
42 |
extras = [ "standard" ]
|
43 |
version = "^0.23.2"
|
44 |
|
45 |
-
[tool.poetry.dependencies.sqlalchemy]
|
46 |
-
extras = [ "asyncio" ]
|
47 |
-
version = "^2.0.30"
|
48 |
-
|
49 |
[tool.poetry.dependencies.llama-index-agent-openai]
|
50 |
version = "0.2.2"
|
51 |
|
52 |
-
[tool.black]
|
53 |
-
line-length = 119
|
54 |
-
|
55 |
-
[tool.pytest.ini_options]
|
56 |
-
asyncio_mode = "auto"
|
57 |
-
|
58 |
[build-system]
|
59 |
requires = [ "poetry-core" ]
|
60 |
build-backend = "poetry.core.masonry.api"
|
|
|
3 |
name = "app"
|
4 |
version = "0.1.0"
|
5 |
description = ""
|
6 |
+
authors = [ "Marcus Schiesser <mail@marcusschiesser.de>" ]
|
7 |
readme = "README.md"
|
8 |
package-mode = false
|
9 |
|
|
|
28 |
dateparser = "^1.2.0"
|
29 |
pandas = "^2.2.2"
|
30 |
path = "^16.14.0"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
32 |
[tool.poetry.dependencies.uvicorn]
|
33 |
extras = [ "standard" ]
|
34 |
version = "^0.23.2"
|
35 |
|
|
|
|
|
|
|
|
|
36 |
[tool.poetry.dependencies.llama-index-agent-openai]
|
37 |
version = "0.2.2"
|
38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
[build-system]
|
40 |
requires = [ "poetry-core" ]
|
41 |
build-backend = "poetry.core.masonry.api"
|
tests/conftest.py
DELETED
@@ -1,83 +0,0 @@
|
|
1 |
-
# Credits: https://github.com/ThomasAitken/demo-fastapi-async-sqlalchemy/blob/main/backend/app/conftest.py
|
2 |
-
|
3 |
-
import asyncio
|
4 |
-
from contextlib import ExitStack
|
5 |
-
|
6 |
-
import pytest
|
7 |
-
from alembic.config import Config
|
8 |
-
from alembic.migration import MigrationContext
|
9 |
-
from alembic.operations import Operations
|
10 |
-
from alembic.script import ScriptDirectory
|
11 |
-
from config.index import config as settings
|
12 |
-
from app.engine.postgresdb import Base, get_db_session, postgresdb as sessionmanager
|
13 |
-
from main import init_app
|
14 |
-
from asyncpg import Connection
|
15 |
-
from fastapi.testclient import TestClient
|
16 |
-
from pytest_postgresql import factories
|
17 |
-
from pytest_postgresql.factories.noprocess import postgresql_noproc
|
18 |
-
from pytest_postgresql.janitor import DatabaseJanitor
|
19 |
-
from sqlalchemy.testing.entities import ComparableEntity
|
20 |
-
|
21 |
-
from config.index import config as env
|
22 |
-
|
23 |
-
test_db = factories.postgresql_proc(dbname="test_db", port=5433)
|
24 |
-
|
25 |
-
|
26 |
-
@pytest.fixture(autouse=True)
|
27 |
-
def app():
|
28 |
-
with ExitStack():
|
29 |
-
# Don't initialize database connection.
|
30 |
-
# This is because we want to initialize the database connection manually, so that we can create the test database.
|
31 |
-
yield init_app(init_db=False)
|
32 |
-
|
33 |
-
|
34 |
-
@pytest.fixture
|
35 |
-
def client(app):
|
36 |
-
with TestClient(app) as c:
|
37 |
-
yield c
|
38 |
-
|
39 |
-
|
40 |
-
@pytest.fixture(scope="session")
|
41 |
-
def event_loop(request):
|
42 |
-
loop = asyncio.get_event_loop_policy().new_event_loop()
|
43 |
-
yield loop
|
44 |
-
loop.close()
|
45 |
-
|
46 |
-
|
47 |
-
@pytest.fixture(scope="function", autouse=True)
|
48 |
-
async def connection_test(test_db, event_loop):
|
49 |
-
pg_host = test_db.host
|
50 |
-
pg_port = test_db.port
|
51 |
-
pg_user = test_db.user
|
52 |
-
pg_db = test_db.dbname
|
53 |
-
pg_password = test_db.password
|
54 |
-
|
55 |
-
with DatabaseJanitor(user=pg_user, host=pg_host, port=pg_port, dbname=pg_db, version=test_db.version, password=pg_password):
|
56 |
-
connection_str = f"postgresql+psycopg://{pg_user}:@{pg_host}:{pg_port}/{pg_db}"
|
57 |
-
sessionmanager.init(connection_str,
|
58 |
-
# {"echo": True, "future": True}
|
59 |
-
)
|
60 |
-
yield
|
61 |
-
await sessionmanager.close()
|
62 |
-
|
63 |
-
|
64 |
-
@pytest.fixture(scope="function", autouse=True)
|
65 |
-
async def create_tables(connection_test):
|
66 |
-
async with sessionmanager.connect() as connection:
|
67 |
-
await sessionmanager.drop_all(connection)
|
68 |
-
await sessionmanager.create_all(connection)
|
69 |
-
|
70 |
-
|
71 |
-
@pytest.fixture(scope="function", autouse=True)
|
72 |
-
async def session_override(app, connection_test):
|
73 |
-
async def get_db_session_override():
|
74 |
-
async with sessionmanager.session() as session:
|
75 |
-
yield session
|
76 |
-
|
77 |
-
app.dependency_overrides[get_db_session] = get_db_session_override
|
78 |
-
|
79 |
-
|
80 |
-
@pytest.fixture(scope="function", autouse=True)
|
81 |
-
async def get_db_session_fixture():
|
82 |
-
async with sessionmanager.session() as session:
|
83 |
-
yield session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/pytest.ini
DELETED
@@ -1,4 +0,0 @@
|
|
1 |
-
[pytest]
|
2 |
-
asyncio_mode = auto
|
3 |
-
timeout = 60
|
4 |
-
addopts = -vv --disable-warnings --durations=10 --durations-min=1.0
|
|
|
|
|
|
|
|
|
|
tests/test_income_statement.py
DELETED
@@ -1,51 +0,0 @@
|
|
1 |
-
from fastapi.testclient import TestClient
|
2 |
-
import pytest
|
3 |
-
|
4 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
5 |
-
from app.model.transaction import Transaction
|
6 |
-
from app.model.user import User
|
7 |
-
from tests.utils import get_fake_transactions
|
8 |
-
|
9 |
-
@pytest.mark.asyncio
|
10 |
-
async def test_income_statement(client: TestClient, get_db_session_fixture: AsyncSession) -> None:
|
11 |
-
session_override = get_db_session_fixture
|
12 |
-
|
13 |
-
# 1. Create a user
|
14 |
-
user = await User.create(session_override, name="user", email="email", hashed_password="password")
|
15 |
-
|
16 |
-
# 2. Create a bunch of transactions
|
17 |
-
fake_transactions = get_fake_transactions(user.id)
|
18 |
-
await Transaction.bulk_create(session_override, fake_transactions)
|
19 |
-
|
20 |
-
# 3. Create an income statement
|
21 |
-
min_date = min(t.transaction_date for t in fake_transactions)
|
22 |
-
max_date = max(t.transaction_date for t in fake_transactions)
|
23 |
-
|
24 |
-
print(f"min_date: {min_date}, max_date: {max_date}")
|
25 |
-
response = client.post(
|
26 |
-
"/api/v1/income_statement",
|
27 |
-
json={
|
28 |
-
"user_id": 1,
|
29 |
-
"date_from": str(min_date),
|
30 |
-
"date_to": str(max_date),
|
31 |
-
},
|
32 |
-
)
|
33 |
-
|
34 |
-
assert response.status_code == 200
|
35 |
-
|
36 |
-
# 4. Verify that the income statement matches the transactions
|
37 |
-
response = client.get(f"/api/v1/income_statement/user/1")
|
38 |
-
print(response.json())
|
39 |
-
assert response.status_code == 200
|
40 |
-
assert response.json()[0].get("income")
|
41 |
-
assert response.json()[0].get("expenses")
|
42 |
-
|
43 |
-
report_id = response.json()[0].get("id")
|
44 |
-
|
45 |
-
# # 5. Verify that the income statement can be retrieved
|
46 |
-
if report_id is not None:
|
47 |
-
response = client.get(f"/api/v1/income_statement/report/{report_id}")
|
48 |
-
assert response.status_code == 200
|
49 |
-
assert response.json().get("income")
|
50 |
-
assert response.json().get("expenses")
|
51 |
-
assert response.json().get("id") == report_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_transactions.py
DELETED
@@ -1,25 +0,0 @@
|
|
1 |
-
from fastapi.testclient import TestClient
|
2 |
-
import pytest
|
3 |
-
|
4 |
-
from app.model.transaction import Transaction
|
5 |
-
from app.model.user import User
|
6 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
7 |
-
from tests.utils import get_fake_transactions
|
8 |
-
|
9 |
-
|
10 |
-
@pytest.mark.asyncio
|
11 |
-
async def test_transactions(client: TestClient, get_db_session_fixture: AsyncSession) -> None:
|
12 |
-
|
13 |
-
session_override = get_db_session_fixture
|
14 |
-
|
15 |
-
# 1. Create a user
|
16 |
-
user = await User.create(session_override, name="user", email="email", hashed_password="password")
|
17 |
-
|
18 |
-
# 2. Create a bunch of transactions
|
19 |
-
fake_transactions = get_fake_transactions(user.id)
|
20 |
-
await Transaction.bulk_create(session_override, fake_transactions)
|
21 |
-
|
22 |
-
# 3. Verify that the transactions are returned
|
23 |
-
response = client.get("/api/v1/transactions/1")
|
24 |
-
assert response.status_code == 200
|
25 |
-
assert len(response.json()) == 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_users.py
DELETED
@@ -1,40 +0,0 @@
|
|
1 |
-
from random import randint
|
2 |
-
|
3 |
-
from fastapi.testclient import TestClient
|
4 |
-
|
5 |
-
|
6 |
-
def test_users(client: TestClient) -> None:
|
7 |
-
rand_int = randint(0, 1000)
|
8 |
-
email = f"test{rand_int}@example.com"
|
9 |
-
response = client.get(f"/api/v1/users/{email}")
|
10 |
-
assert response.status_code == 404
|
11 |
-
|
12 |
-
response = client.post(
|
13 |
-
"/api/v1/users",
|
14 |
-
json={"email": email, "name": "Full Name Test", "hashed_password": "test"},
|
15 |
-
)
|
16 |
-
assert response.status_code == 200
|
17 |
-
assert response.json().get("email") == email
|
18 |
-
assert response.json().get("name") == "Full Name Test"
|
19 |
-
assert response.json().get("is_deleted") == False
|
20 |
-
|
21 |
-
response = client.get(f"/api/v1/users/{email}")
|
22 |
-
assert response.status_code == 200
|
23 |
-
assert response.json().get("email") == email
|
24 |
-
assert response.json().get("name") == "Full Name Test"
|
25 |
-
assert response.json().get("is_deleted") == False
|
26 |
-
|
27 |
-
response = client.put(
|
28 |
-
f"/api/v1/users/{email}",
|
29 |
-
json={"email": email, "name": "Full Name Test 2", "hashed_password": "test"},
|
30 |
-
)
|
31 |
-
assert response.status_code == 200
|
32 |
-
assert response.json().get("email") == email
|
33 |
-
assert response.json().get("name") == "Full Name Test 2"
|
34 |
-
assert response.json().get("is_deleted") == False
|
35 |
-
|
36 |
-
response = client.delete(f"/api/v1/users/{email}")
|
37 |
-
assert response.status_code == 200
|
38 |
-
assert response.json().get("email") == email
|
39 |
-
assert response.json().get("name") == "Full Name Test 2"
|
40 |
-
assert response.json().get("is_deleted") == True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/utils.py
DELETED
@@ -1,88 +0,0 @@
|
|
1 |
-
from datetime import datetime
|
2 |
-
from typing import List
|
3 |
-
from app.schema.index import TransactionCreate, TransactionType
|
4 |
-
|
5 |
-
|
6 |
-
def get_fake_transactions(user_id: int) -> List[TransactionCreate]:
|
7 |
-
return [
|
8 |
-
TransactionCreate(
|
9 |
-
user_id=user_id,
|
10 |
-
transaction_date=datetime(2022, 1, 1),
|
11 |
-
category="category1",
|
12 |
-
name_description="name_description",
|
13 |
-
amount=1.0,
|
14 |
-
type=TransactionType.EXPENSE,
|
15 |
-
),
|
16 |
-
TransactionCreate(
|
17 |
-
user_id=user_id,
|
18 |
-
transaction_date=datetime(2022, 1, 2),
|
19 |
-
category="category2",
|
20 |
-
name_description="name_description",
|
21 |
-
amount=2.0,
|
22 |
-
type=TransactionType.EXPENSE,
|
23 |
-
),
|
24 |
-
TransactionCreate(
|
25 |
-
user_id=user_id,
|
26 |
-
transaction_date=datetime(2022, 1, 3),
|
27 |
-
category="category3",
|
28 |
-
name_description="name_description",
|
29 |
-
amount=3.0,
|
30 |
-
type=TransactionType.INCOME,
|
31 |
-
),
|
32 |
-
TransactionCreate(
|
33 |
-
user_id=user_id,
|
34 |
-
transaction_date=datetime(2022, 1, 4),
|
35 |
-
category="category1",
|
36 |
-
name_description="name_description",
|
37 |
-
amount=4.0,
|
38 |
-
type=TransactionType.INCOME,
|
39 |
-
),
|
40 |
-
TransactionCreate(
|
41 |
-
user_id=user_id,
|
42 |
-
transaction_date=datetime(2022, 1, 5),
|
43 |
-
category="category2",
|
44 |
-
name_description="name_description",
|
45 |
-
amount=5.0,
|
46 |
-
type=TransactionType.EXPENSE,
|
47 |
-
),
|
48 |
-
TransactionCreate(
|
49 |
-
user_id=user_id,
|
50 |
-
transaction_date=datetime(2022, 1, 6),
|
51 |
-
category="category3",
|
52 |
-
name_description="name_description",
|
53 |
-
amount=6.0,
|
54 |
-
type=TransactionType.EXPENSE,
|
55 |
-
),
|
56 |
-
TransactionCreate(
|
57 |
-
user_id=user_id,
|
58 |
-
transaction_date=datetime(2022, 1, 7),
|
59 |
-
category="category1",
|
60 |
-
name_description="name_description",
|
61 |
-
amount=7.0,
|
62 |
-
type=TransactionType.INCOME,
|
63 |
-
),
|
64 |
-
TransactionCreate(
|
65 |
-
user_id=user_id,
|
66 |
-
transaction_date=datetime(2022, 1, 8),
|
67 |
-
category="category2",
|
68 |
-
name_description="name_description",
|
69 |
-
amount=8.0,
|
70 |
-
type=TransactionType.INCOME,
|
71 |
-
),
|
72 |
-
TransactionCreate(
|
73 |
-
user_id=user_id,
|
74 |
-
transaction_date=datetime(2022, 1, 9),
|
75 |
-
category="category3",
|
76 |
-
name_description="name_description",
|
77 |
-
amount=9.0,
|
78 |
-
type=TransactionType.EXPENSE,
|
79 |
-
),
|
80 |
-
TransactionCreate(
|
81 |
-
user_id=user_id,
|
82 |
-
transaction_date=datetime(2022, 1, 10),
|
83 |
-
category="category1",
|
84 |
-
name_description="name_description",
|
85 |
-
amount=10.0,
|
86 |
-
type=TransactionType.EXPENSE,
|
87 |
-
),
|
88 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|