postgres database integration

#3
.dockerignore CHANGED
@@ -1,3 +1,4 @@
1
  __pycache__
2
  storage
3
- data/*
 
 
1
  __pycache__
2
  storage
3
+ data/*
4
+ venv
.env.example CHANGED
@@ -43,3 +43,11 @@ APP_HOST=0.0.0.0
43
 
44
  # The port to start the backend app.
45
  APP_PORT=8000
 
 
 
 
 
 
 
 
 
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=codepath_project_postgres_local
51
+ POSTGRES_DB_HOST=localhost
52
+ POSTGRES_DB_PORT=5432
53
+ SQLALCHEMY_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_DB_HOST}:${POSTGRES_DB_PORT}/${POSTGRES_DB_NAME}
.gitignore CHANGED
@@ -1,4 +1,5 @@
1
  __pycache__
2
  storage
3
  .env
4
- data/*
 
 
1
  __pycache__
2
  storage
3
  .env
4
+ data/*
5
+ venv
.vscode/settings.json CHANGED
@@ -1,3 +1,11 @@
1
  {
2
- "jupyter.notebookFileRoot": "${workspaceFolder}"
 
 
 
 
 
 
 
 
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
  }
Dockerfile.local.postgres ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
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
README.md CHANGED
@@ -69,6 +69,36 @@ The API allows CORS for all origins to simplify development. You can change this
69
  ENVIRONMENT=prod python main.py
70
  ```
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  ## Using Docker
73
 
74
  1. Build an image for the FastAPI app:
 
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
+ alembic revision --autogenerate -m "<your_comment>"
94
+ ```
95
+
96
+ To locally verify your changes, run:
97
+
98
+ ```bash
99
+ alembic upgrade head
100
+ ```
101
+
102
  ## Using Docker
103
 
104
  1. Build an image for the FastAPI app:
alembic.ini ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/user.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from fastapi import APIRouter, Depends, HTTPException
3
+ from sqlalchemy.orm import Session
4
+
5
+ from app.engine.postgresdb import get_db
6
+ from app.schema.index import UserCreate, User as UserSchema
7
+ from app.model.user import User as UserModel
8
+
9
+
10
+ user_router = r = APIRouter(prefix="/api/v1/users", tags=["users"])
11
+
12
+
13
+ @r.post(
14
+ "/",
15
+ response_model=UserSchema,
16
+ responses={
17
+ 200: {"description": "New user created"},
18
+ 400: {"description": "Bad request"},
19
+ 409: {"description": "Conflict"},
20
+ 500: {"description": "Internal server error"},
21
+ },
22
+ )
23
+ async def create_user(user: UserCreate, db: Session = Depends(get_db)):
24
+ try:
25
+ db_user = (
26
+ db.query(UserModel).filter(UserModel.email == user.email).sort_by(UserModel.updated_at, desc=True).first()
27
+ )
28
+ if db_user and not db_user.is_deleted:
29
+ raise HTTPException(status_code=409, detail="User already exists")
30
+
31
+ db_user = UserModel(**user.dict())
32
+ db.add(db_user)
33
+ db.commit()
34
+ db.refresh(db_user)
35
+ return db_user
36
+ except Exception as e:
37
+ raise HTTPException(status_code=500, detail=str(e))
38
+
39
+
40
+ @r.get(
41
+ "/{user_id}",
42
+ response_model=UserSchema,
43
+ responses={
44
+ 200: {"description": "User found"},
45
+ 404: {"description": "User not found"},
46
+ 500: {"description": "Internal server error"},
47
+ },
48
+ )
49
+ async def get_user(user_id: int, db: Session = Depends(get_db)):
50
+ user = db.query(UserModel).get(user_id)
51
+ if not user:
52
+ raise HTTPException(status_code=404, detail="User not found")
53
+ return user
54
+
55
+
56
+ @r.put(
57
+ "/{user_id}",
58
+ response_model=UserSchema,
59
+ responses={
60
+ 200: {"description": "User updated"},
61
+ 404: {"description": "User not found"},
62
+ 500: {"description": "Internal server error"},
63
+ },
64
+ )
65
+ async def update_user(user_id: int, user_payload: UserCreate, db: Session = Depends(get_db)):
66
+ user = db.query(UserModel).get(user_id)
67
+ if not user:
68
+ raise HTTPException(status_code=404, detail="User not found")
69
+ user.name = user_payload.name
70
+ user.email = user_payload.email
71
+ db.commit()
72
+ db.refresh(user)
73
+ return user
74
+
75
+
76
+ @r.delete(
77
+ "/{user_id}",
78
+ response_model=UserSchema,
79
+ responses={
80
+ 200: {"description": "User deleted"},
81
+ 404: {"description": "User not found"},
82
+ 500: {"description": "Internal server error"},
83
+ },
84
+ )
85
+ async def delete_user(user_id: int, db: Session = Depends(get_db)):
86
+ user = db.query(UserModel).get(user_id)
87
+ if not user:
88
+ raise HTTPException(status_code=404, detail="User not found")
89
+ user.is_deleted = True
90
+ db.commit()
91
+ db.refresh(user)
92
+ return user
app/engine/postgresdb.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from sqlalchemy import create_engine
4
+ from sqlalchemy.orm import sessionmaker, declarative_base, Session
5
+ from fastapi import Request
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+ logger = logging.getLogger(__name__)
11
+
12
+ SQLALCHEMY_DATABASE_URL = os.getenv("SQLALCHEMY_DATABASE_URL")
13
+ assert SQLALCHEMY_DATABASE_URL, f"SQLALCHEMY_DATABASE_URL is not set"
14
+ logger.info(f"SQLALCHEMY_DATABASE_URL: {SQLALCHEMY_DATABASE_URL}")
15
+
16
+ engine = create_engine(SQLALCHEMY_DATABASE_URL)
17
+
18
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
19
+
20
+ Base = declarative_base()
21
+
22
+
23
+ # def get_db():
24
+ # session = SessionLocal()
25
+ # logger.info("Postgres Session created successfully!")
26
+ # try:
27
+ # yield session
28
+ # finally:
29
+ # session.close()
30
+ # logger.info("Postgres Session closed successfully!")
31
+
32
+
33
+ # Dependency
34
+ def get_db(request: Request):
35
+ return request.state.db
app/model/base.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from sqlalchemy import Column, DateTime, Integer
3
+
4
+ from app.engine.postgresdb import Base
5
+
6
+ class BaseModel():
7
+ id = Column(Integer, primary_key=True, index=True)
8
+ created_at = Column(DateTime, default=datetime.now())
9
+ updated_at = Column(DateTime, default=datetime.now(), onupdate=datetime.now())
app/model/transaction.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String
2
+ from sqlalchemy.orm import relationship
3
+
4
+ from app.model.base import BaseModel
5
+ from app.engine.postgresdb import Base
6
+
7
+ class Transaction(Base, BaseModel):
8
+ __tablename__ = "transactions"
9
+ transaction_date = Column(DateTime)
10
+ category = Column(String)
11
+ name_description = Column(String)
12
+ amount = Column(Float)
13
+ type = Column(String)
14
+
15
+ user_id = Column(Integer, ForeignKey("users.id"))
16
+ user = relationship("User", back_populates="transactions")
app/model/user.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, String, Boolean
2
+ from sqlalchemy.orm import relationship
3
+
4
+ from app.model.base import BaseModel
5
+ from app.engine.postgresdb import Base
6
+
7
+
8
+ class User(Base, BaseModel):
9
+ __tablename__ = "users"
10
+
11
+ name = Column(String)
12
+ email = Column(String)
13
+ hashed_password = Column(String)
14
+ is_deleted = Column(Boolean, default=False)
15
+
16
+ transactions = relationship("Transaction", back_populates="user")
app/schema/base.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class BaseModel(BaseModel):
6
+ id: int
7
+ created_at: datetime
8
+ updated_at: datetime
9
+
10
+ class Config:
11
+ from_attributes = True
app/schema/index.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+ from datetime import datetime
3
+ from typing import List
4
+
5
+ from app.schema.base import BaseModel
6
+
7
+
8
+ class TransactionType(str, Enum):
9
+ INCOME = "income"
10
+ EXPENSE = "expense"
11
+
12
+
13
+ class UserCreate(BaseModel):
14
+ name: str
15
+ email: str
16
+ hashed_password: str
17
+
18
+
19
+ class User(BaseModel):
20
+ name: str
21
+ email: str
22
+ hashed_password: str
23
+ is_deleted: bool = False
24
+ transactions: "List[Transaction]"
25
+
26
+
27
+ class Transaction(BaseModel):
28
+ transaction_date: datetime
29
+ category: str
30
+ name_description: str
31
+ amount: float
32
+ type: TransactionType
33
+ user: User
main.py CHANGED
@@ -5,16 +5,54 @@ load_dotenv()
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()
@@ -22,7 +60,6 @@ init_observability()
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,
@@ -41,6 +78,7 @@ if environment == "dev":
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__":
 
5
  import logging
6
  import os
7
  import uvicorn
8
+ from contextlib import asynccontextmanager
9
+ from fastapi import FastAPI, Request, Response
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from fastapi.responses import RedirectResponse
12
  from app.api.routers.chat import chat_router
13
+ from app.api.routers.user import user_router
14
  from app.settings import init_settings
15
  from app.observability import init_observability
16
  from fastapi.staticfiles import StaticFiles
17
+ from alembic.config import Config
18
+ from alembic import command
19
 
20
 
21
+ from app.engine.postgresdb import engine, Base, SessionLocal
22
+
23
+ logger = logging.getLogger("uvicorn")
24
+
25
+ # Create all tables in the database
26
+ Base.metadata.create_all(bind=engine)
27
+
28
+
29
+ def run_migrations():
30
+ alembic_cfg = Config("alembic.ini")
31
+ command.upgrade(alembic_cfg, "head")
32
+
33
+
34
+ @asynccontextmanager
35
+ async def lifespan(app_: FastAPI):
36
+ logger.info("Starting up...")
37
+ logger.info("Run 'alembic upgrade head' to apply migrations...")
38
+ run_migrations()
39
+ yield
40
+ logger.info("Shutting down...")
41
+
42
+
43
+ app = FastAPI(lifespan=lifespan)
44
+
45
+
46
+ @app.middleware("http")
47
+ async def db_session_middleware(request: Request, call_next):
48
+ response = Response("Internal server error", status_code=500)
49
+ try:
50
+ request.state.db = SessionLocal()
51
+ response = await call_next(request)
52
+ finally:
53
+ request.state.db.close()
54
+ return response
55
+
56
 
57
  init_settings()
58
  init_observability()
 
60
  environment = os.getenv("ENVIRONMENT", "dev") # Default to 'development' if not set
61
 
62
  if environment == "dev":
 
63
  logger.warning("Running in development mode - allowing CORS for all origins")
64
  app.add_middleware(
65
  CORSMiddleware,
 
78
  if os.path.exists("data"):
79
  app.mount("/api/data", StaticFiles(directory="data"), name="static")
80
  app.include_router(chat_router, prefix="/api/chat")
81
+ app.include_router(user_router)
82
 
83
 
84
  if __name__ == "__main__":
migration/README ADDED
@@ -0,0 +1 @@
 
 
1
+ Generic single-database configuration.
migration/env.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from logging.config import fileConfig
3
+
4
+ from app.engine.postgresdb import Base
5
+ from sqlalchemy import engine_from_config
6
+ from sqlalchemy import pool
7
+
8
+ from alembic import context
9
+
10
+ from dotenv import load_dotenv
11
+
12
+ load_dotenv()
13
+
14
+ # this is the Alembic Config object, which provides
15
+ # access to the values within the .ini file in use.
16
+ config = context.config
17
+
18
+ config.set_main_option("sqlalchemy.url", os.getenv("SQLALCHEMY_DATABASE_URL"))
19
+
20
+ # Interpret the config file for Python logging.
21
+ # This line sets up loggers basically.
22
+ if config.config_file_name is not None:
23
+ fileConfig(config.config_file_name)
24
+
25
+ # Import all models so they're registered with SQLAlchemy.
26
+ from app.model import *
27
+
28
+ # add your model's MetaData object here
29
+ # for 'autogenerate' support
30
+ # from myapp import mymodel
31
+ # target_metadata = mymodel.Base.metadata
32
+ target_metadata = Base.metadata
33
+
34
+ # other values from the config, defined by the needs of env.py,
35
+ # can be acquired:
36
+ # my_important_option = config.get_main_option("my_important_option")
37
+ # ... etc.
38
+
39
+
40
+ def run_migrations_offline() -> None:
41
+ """Run migrations in 'offline' mode.
42
+
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
+
48
+ Calls to context.execute() here emit the given string to the
49
+ script output.
50
+
51
+ """
52
+ url = config.get_main_option("sqlalchemy.url")
53
+ context.configure(
54
+ url=url,
55
+ target_metadata=target_metadata,
56
+ literal_binds=True,
57
+ dialect_opts={"paramstyle": "named"},
58
+ )
59
+
60
+ with context.begin_transaction():
61
+ context.run_migrations()
62
+
63
+
64
+ def run_migrations_online() -> None:
65
+ """Run migrations in 'online' mode.
66
+
67
+ In this scenario we need to create an Engine
68
+ and associate a connection with the context.
69
+
70
+ """
71
+ connectable = engine_from_config(
72
+ config.get_section(config.config_ini_section, {}),
73
+ prefix="sqlalchemy.",
74
+ poolclass=pool.NullPool,
75
+ )
76
+
77
+ with connectable.connect() as connection:
78
+ context.configure(
79
+ connection=connection, target_metadata=target_metadata, compare_type=True
80
+ )
81
+
82
+ with context.begin_transaction():
83
+ context.run_migrations()
84
+
85
+
86
+ if context.is_offline_mode():
87
+ run_migrations_offline()
88
+ else:
89
+ run_migrations_online()
migration/script.py.mako ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/cd515c44401d_add_users_transactions_tables.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,6 +123,25 @@ files = [
123
  [package.dependencies]
124
  typing-extensions = "*"
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  [[package]]
127
  name = "annotated-types"
128
  version = "0.7.0"
@@ -441,6 +460,20 @@ files = [
441
  {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
442
  ]
443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  [[package]]
445
  name = "fastapi"
446
  version = "0.109.2"
@@ -925,6 +958,17 @@ files = [
925
  {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"},
926
  ]
927
 
 
 
 
 
 
 
 
 
 
 
 
928
  [[package]]
929
  name = "jinja2"
930
  version = "3.1.4"
@@ -1465,6 +1509,25 @@ files = [
1465
  httpx = ">=0.20.0"
1466
  pydantic = ">=1.10"
1467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1468
  [[package]]
1469
  name = "markupsafe"
1470
  version = "2.1.5"
@@ -2540,6 +2603,21 @@ urllib3 = {version = ">=1.26.0", markers = "python_version >= \"3.8\" and python
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,6 +2661,87 @@ files = [
2583
  {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"},
2584
  ]
2585
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2586
  [[package]]
2587
  name = "pydantic"
2588
  version = "2.7.2"
@@ -2711,6 +2870,64 @@ docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"]
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"
@@ -3993,4 +4210,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more
3993
  [metadata]
3994
  lock-version = "2.0"
3995
  python-versions = "^3.11,<3.12"
3996
- content-hash = "764a6a2edfd938e4128d185bb383ddc23dadaa25b9f5cf7215998a90b3cc760e"
 
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"
 
460
  {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
461
  ]
462
 
463
+ [[package]]
464
+ name = "execnet"
465
+ version = "2.1.1"
466
+ description = "execnet: rapid multi-Python deployment"
467
+ optional = false
468
+ python-versions = ">=3.8"
469
+ files = [
470
+ {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"},
471
+ {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"},
472
+ ]
473
+
474
+ [package.extras]
475
+ testing = ["hatch", "pre-commit", "pytest", "tox"]
476
+
477
  [[package]]
478
  name = "fastapi"
479
  version = "0.109.2"
 
958
  {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"},
959
  ]
960
 
961
+ [[package]]
962
+ name = "iniconfig"
963
+ version = "2.0.0"
964
+ description = "brain-dead simple config-ini parsing"
965
+ optional = false
966
+ python-versions = ">=3.7"
967
+ files = [
968
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
969
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
970
+ ]
971
+
972
  [[package]]
973
  name = "jinja2"
974
  version = "3.1.4"
 
1509
  httpx = ">=0.20.0"
1510
  pydantic = ">=1.10"
1511
 
1512
+ [[package]]
1513
+ name = "mako"
1514
+ version = "1.3.5"
1515
+ description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
1516
+ optional = false
1517
+ python-versions = ">=3.8"
1518
+ files = [
1519
+ {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"},
1520
+ {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"},
1521
+ ]
1522
+
1523
+ [package.dependencies]
1524
+ MarkupSafe = ">=0.9.2"
1525
+
1526
+ [package.extras]
1527
+ babel = ["Babel"]
1528
+ lingua = ["lingua"]
1529
+ testing = ["pytest"]
1530
+
1531
  [[package]]
1532
  name = "markupsafe"
1533
  version = "2.1.5"
 
2603
  [package.extras]
2604
  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)"]
2605
 
2606
+ [[package]]
2607
+ name = "pluggy"
2608
+ version = "1.5.0"
2609
+ description = "plugin and hook calling mechanisms for python"
2610
+ optional = false
2611
+ python-versions = ">=3.8"
2612
+ files = [
2613
+ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
2614
+ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
2615
+ ]
2616
+
2617
+ [package.extras]
2618
+ dev = ["pre-commit", "tox"]
2619
+ testing = ["pytest", "pytest-benchmark"]
2620
+
2621
  [[package]]
2622
  name = "posthog"
2623
  version = "3.5.0"
 
2661
  {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"},
2662
  ]
2663
 
2664
+ [[package]]
2665
+ name = "psycopg2-binary"
2666
+ version = "2.9.9"
2667
+ description = "psycopg2 - Python-PostgreSQL Database Adapter"
2668
+ optional = false
2669
+ python-versions = ">=3.7"
2670
+ files = [
2671
+ {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"},
2672
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"},
2673
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"},
2674
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"},
2675
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"},
2676
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"},
2677
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"},
2678
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"},
2679
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"},
2680
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"},
2681
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"},
2682
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"},
2683
+ {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"},
2684
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"},
2685
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"},
2686
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"},
2687
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"},
2688
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"},
2689
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"},
2690
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"},
2691
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"},
2692
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"},
2693
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"},
2694
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"},
2695
+ {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"},
2696
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"},
2697
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"},
2698
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"},
2699
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"},
2700
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"},
2701
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"},
2702
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"},
2703
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"},
2704
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"},
2705
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"},
2706
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"},
2707
+ {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"},
2708
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"},
2709
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"},
2710
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"},
2711
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"},
2712
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"},
2713
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"},
2714
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"},
2715
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"},
2716
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"},
2717
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"},
2718
+ {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"},
2719
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"},
2720
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"},
2721
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"},
2722
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"},
2723
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"},
2724
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"},
2725
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"},
2726
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"},
2727
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"},
2728
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"},
2729
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"},
2730
+ {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"},
2731
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"},
2732
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"},
2733
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"},
2734
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"},
2735
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"},
2736
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"},
2737
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"},
2738
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"},
2739
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"},
2740
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"},
2741
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"},
2742
+ {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"},
2743
+ ]
2744
+
2745
  [[package]]
2746
  name = "pydantic"
2747
  version = "2.7.2"
 
2870
  full = ["Pillow (>=8.0.0)", "PyCryptodome", "cryptography"]
2871
  image = ["Pillow (>=8.0.0)"]
2872
 
2873
+ [[package]]
2874
+ name = "pytest"
2875
+ version = "8.2.1"
2876
+ description = "pytest: simple powerful testing with Python"
2877
+ optional = false
2878
+ python-versions = ">=3.8"
2879
+ files = [
2880
+ {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
2881
+ {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
2882
+ ]
2883
+
2884
+ [package.dependencies]
2885
+ colorama = {version = "*", markers = "sys_platform == \"win32\""}
2886
+ iniconfig = "*"
2887
+ packaging = "*"
2888
+ pluggy = ">=1.5,<2.0"
2889
+
2890
+ [package.extras]
2891
+ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
2892
+
2893
+ [[package]]
2894
+ name = "pytest-asyncio"
2895
+ version = "0.23.7"
2896
+ description = "Pytest support for asyncio"
2897
+ optional = false
2898
+ python-versions = ">=3.8"
2899
+ files = [
2900
+ {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
2901
+ {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
2902
+ ]
2903
+
2904
+ [package.dependencies]
2905
+ pytest = ">=7.0.0,<9"
2906
+
2907
+ [package.extras]
2908
+ docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
2909
+ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
2910
+
2911
+ [[package]]
2912
+ name = "pytest-xdist"
2913
+ version = "3.6.1"
2914
+ description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs"
2915
+ optional = false
2916
+ python-versions = ">=3.8"
2917
+ files = [
2918
+ {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"},
2919
+ {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"},
2920
+ ]
2921
+
2922
+ [package.dependencies]
2923
+ execnet = ">=2.1"
2924
+ pytest = ">=7.0.0"
2925
+
2926
+ [package.extras]
2927
+ psutil = ["psutil (>=3.0)"]
2928
+ setproctitle = ["setproctitle"]
2929
+ testing = ["filelock"]
2930
+
2931
  [[package]]
2932
  name = "python-dateutil"
2933
  version = "2.9.0.post0"
 
4210
  [metadata]
4211
  lock-version = "2.0"
4212
  python-versions = "^3.11,<3.12"
4213
+ content-hash = "527fd744fa60d7638490e59133486e40e1a5303635e9f0fd7286ce5ec97ad4d1"
pyproject.toml CHANGED
@@ -28,6 +28,11 @@ pydantic = "^2.7.2"
28
  dateparser = "^1.2.0"
29
  pandas = "^2.2.2"
30
  path = "^16.14.0"
 
 
 
 
 
31
 
32
  [tool.poetry.dependencies.uvicorn]
33
  extras = [ "standard" ]
@@ -36,6 +41,9 @@ version = "^0.23.2"
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"
 
28
  dateparser = "^1.2.0"
29
  pandas = "^2.2.2"
30
  path = "^16.14.0"
31
+ psycopg2-binary = "^2.9.9"
32
+ pytest-xdist = "^3.6.1"
33
+ pytest-asyncio = "^0.23.7"
34
+ httpx = "^0.27.0"
35
+ alembic = "^1.13.1"
36
 
37
  [tool.poetry.dependencies.uvicorn]
38
  extras = [ "standard" ]
 
41
  [tool.poetry.dependencies.llama-index-agent-openai]
42
  version = "0.2.2"
43
 
44
+ [tool.black]
45
+ line-length = 119
46
+
47
  [build-system]
48
  requires = [ "poetry-core" ]
49
  build-backend = "poetry.core.masonry.api"
tests/test_main.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from main import app
2
+ from fastapi.testclient import TestClient
3
+
4
+ client = TestClient(app)
5
+
6
+
7
+ def test_read_main():
8
+ response = client.get("/")
9
+ assert response.status_code == 200
tests/test_postgresdb_connection.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from app.engine.postgresdb import get_db
3
+
4
+
5
+ @pytest.mark.skip
6
+ def test_postgres_db_connection():
7
+ """
8
+ Tests the connection to the postgres db
9
+ """
10
+ with get_db() as session:
11
+ # Execute a query to test the connection
12
+ result = session.execute(f"SELECT 1").scalar()
13
+
14
+ # Assert that the result is equal to 1
15
+ assert result == 1