From 3a1de6834685c5b81783bc40db45eea71fb33905 Mon Sep 17 00:00:00 2001 From: Armolas Date: Thu, 18 Jul 2024 20:23:53 +0100 Subject: [PATCH 01/10] created database schema for users, organisations, profile, product --- alembic.ini | 116 ++++++++++++++++++ alembic/env.py | 24 ++-- .../9670771a11f9_initial_migrations.py | 81 ++++++++++++ api/db/database.py | 23 ++-- api/v1/models/base.py | 20 +++ api/v1/models/org.py | 36 ++++++ api/v1/models/product.py | 24 ++++ api/v1/models/profile.py | 30 +++++ api/v1/models/user.py | 61 +++++++++ database.db | Bin 0 -> 40960 bytes db_test.py | 41 +++++++ 11 files changed, 429 insertions(+), 27 deletions(-) create mode 100644 alembic.ini create mode 100644 alembic/versions/9670771a11f9_initial_migrations.py create mode 100644 api/v1/models/base.py create mode 100644 api/v1/models/org.py create mode 100644 api/v1/models/product.py create mode 100644 api/v1/models/profile.py create mode 100644 api/v1/models/user.py create mode 100644 database.db create mode 100644 db_test.py diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 000000000..ae1dc8be9 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,116 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = sqlite:///./database.db + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py index b78cb58bd..b98bf4f22 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,16 +1,12 @@ - -import os -from alembic import context -from decouple import config from logging.config import fileConfig from sqlalchemy import engine_from_config from sqlalchemy import pool -from api.db.database import Base -from api.v1.models.auth import User, BlackListToken - -#from db.database import DATABASE_URL -DATABASE_URL=config('DB_URL') - +from alembic import context +from api.v1.models.user import User +from api.v1.models.org import Organisation +from api.v1.models.profile import Profile +from api.v1.models.product import Product +from api.v1.models.base import Base # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -45,7 +41,7 @@ def run_migrations_offline() -> None: script output. """ - url = config.get_main_option("sqlalchemy.url", DATABASE_URL) + url = config.get_main_option("sqlalchemy.url") context.configure( url=url, target_metadata=target_metadata, @@ -64,12 +60,8 @@ def run_migrations_online() -> None: and associate a connection with the context. """ - - alembic_config = config.get_section(config.config_ini_section) - alembic_config['sqlalchemy.url'] = DATABASE_URL - connectable = engine_from_config( - alembic_config, + config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", poolclass=pool.NullPool, ) diff --git a/alembic/versions/9670771a11f9_initial_migrations.py b/alembic/versions/9670771a11f9_initial_migrations.py new file mode 100644 index 000000000..644c854c6 --- /dev/null +++ b/alembic/versions/9670771a11f9_initial_migrations.py @@ -0,0 +1,81 @@ +"""Initial migrations + +Revision ID: 9670771a11f9 +Revises: +Create Date: 2024-07-18 20:08:23.466008 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '9670771a11f9' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('organisations', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('products', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('price', sa.Numeric(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('username', sa.String(length=50), nullable=False), + sa.Column('email', sa.String(length=100), nullable=False), + sa.Column('password', sa.String(length=255), nullable=False), + sa.Column('first_name', sa.String(length=50), nullable=True), + sa.Column('last_name', sa.String(length=50), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('username') + ) + op.create_table('profiles', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('bio', sa.Text(), nullable=True), + sa.Column('phone_number', sa.String(length=50), nullable=True), + sa.Column('avatar_url', sa.String(length=100), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user_organisation', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('organisation_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['organisation_id'], ['organisations.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('user_id', 'organisation_id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user_organisation') + op.drop_table('profiles') + op.drop_table('users') + op.drop_table('products') + op.drop_table('organisations') + # ### end Alembic commands ### diff --git a/api/db/database.py b/api/db/database.py index 472829eca..d53709632 100644 --- a/api/db/database.py +++ b/api/db/database.py @@ -1,19 +1,23 @@ -# database.py +#!/usr/bin/env python3 +""" The database module +""" from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from decouple import config +from contextlib import contextmanager +from api.v1.models.base import Base def get_db_engine(): - DB_TYPE = config("DB_TYPE") - DB_NAME = config("DB_NAME") - DB_USER = config("DB_USER") - DB_PASSWORD = config("DB_PASSWORD") - DB_HOST = config("DB_HOST") - DB_PORT = config("DB_PORT") - MYSQL_DRIVER = config("MYSQL_DRIVER") + DB_TYPE = config("DB_TYPE", "") + DB_NAME = config("DB_NAME", "") + DB_USER = config("DB_USER", "") + DB_PASSWORD = config("DB_PASSWORD", "") + DB_HOST = config("DB_HOST", "") + DB_PORT = config("DB_PORT", "") + MYSQL_DRIVER = config("MYSQL_DRIVER", "") DATABASE_URL = "" if DB_TYPE == "mysql": @@ -35,13 +39,10 @@ def get_db_engine(): SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=db_engine) -Base = declarative_base() - def create_database(): return Base.metadata.create_all(bind=db_engine) - def get_db(): db = SessionLocal() try: diff --git a/api/v1/models/base.py b/api/v1/models/base.py new file mode 100644 index 000000000..cf75a1544 --- /dev/null +++ b/api/v1/models/base.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +""" Base +""" +from sqlalchemy import ( + Column, + Integer, + ForeignKey, + Table + ) +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship +from datetime import datetime + + +Base = declarative_base() + +user_organisation_association = Table('user_organisation', Base.metadata, + Column('user_id', Integer, ForeignKey('users.id'), primary_key=True), + Column('organisation_id', Integer, ForeignKey('organisations.id'), primary_key=True) + ) diff --git a/api/v1/models/org.py b/api/v1/models/org.py new file mode 100644 index 000000000..1d3554f75 --- /dev/null +++ b/api/v1/models/org.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +""" The Organisation model +""" +from sqlalchemy import ( + Column, + Integer, + String, + Text, + Date, + ForeignKey, + Numeric, + DateTime, + func, + ) +from sqlalchemy.orm import relationship +from datetime import datetime +from api.v1.models.base import Base, user_organisation_association + + +class Organisation(Base): + __tablename__ = 'organisations' + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(50), unique=True, nullable=False) + description = Column(Text, nullable=True) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + users = relationship( + "User", + secondary=user_organisation_association, + back_populates="organisations" + ) + + def __str__(self): + return self.name diff --git a/api/v1/models/product.py b/api/v1/models/product.py new file mode 100644 index 000000000..3b7e8509e --- /dev/null +++ b/api/v1/models/product.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +""" The Product model +""" +from sqlalchemy import ( + Column, + Integer, + String, + Text, + Numeric, + DateTime, + func, + ) +from datetime import datetime +from api.v1.models.base import Base + + +class Product(Base): + __tablename__ = 'products' + + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String, nullable=False) + description = Column(Text, nullable=True) + price = Column(Numeric, nullable=False) + created_at = Column(DateTime, nullable=False, default=func.now()) diff --git a/api/v1/models/profile.py b/api/v1/models/profile.py new file mode 100644 index 000000000..ed9bb94ad --- /dev/null +++ b/api/v1/models/profile.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +""" The Profile model +""" +from sqlalchemy import ( + Column, + Integer, + String, + Text, + Date, + ForeignKey, + DateTime, + func, + ) +from sqlalchemy.orm import relationship +from datetime import datetime +from api.v1.models.base import Base + + +class Profile(Base): + __tablename__ = 'profiles' + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + bio = Column(Text, nullable=True) + phone_number = Column(String(50), nullable=True) + avatar_url = Column(String(100), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + #user = relationship("User", back_populates="profile") diff --git a/api/v1/models/user.py b/api/v1/models/user.py new file mode 100644 index 000000000..81d45c080 --- /dev/null +++ b/api/v1/models/user.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" User data model +""" +from sqlalchemy import ( + create_engine, + Column, + Integer, + String, + Text, + Date, + ForeignKey, + Numeric, + DateTime, + func, + Table + ) +from sqlalchemy.orm import relationship +from datetime import datetime +from api.v1.models.base import Base, user_organisation_association +import bcrypt + +def hash_password(password: str) -> bytes: + """ Hashes the user password for security + """ + salt = bcrypt.gensalt() + + hash_pw = bcrypt.hashpw(password.encode(), salt) + return hash_pw + +class User(Base): + __tablename__ = 'users' + + id = Column(Integer, primary_key=True, autoincrement=True) + username = Column(String(50), unique=True, nullable=False) + email = Column(String(100), unique=True, nullable=False) + password = Column(String(255), nullable=False) + first_name = Column(String(50)) + last_name = Column(String(50)) + created_at = Column(DateTime, server_default=func.now()) + updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + + profile = relationship("Profile", uselist=False) + organisations = relationship( + "Organisation", + secondary=user_organisation_association, + back_populates="users" + ) + + def __init__(self, **kwargs): + """ Initializes a user instance + """ + keys = ['username', 'email', 'password', 'first_name', 'last_name'] + for key, value in kwargs.items(): + if key in keys: + if key == 'password': + value = hash_password(value) + setattr(self, key, value) + + def __str__(self): + return self.email + diff --git a/database.db b/database.db new file mode 100644 index 0000000000000000000000000000000000000000..13789cfbba7ea3a1d6167af79b4dd0fdb6709b97 GIT binary patch literal 40960 zcmeI*T~FIq7zc1WHVIA(u2gl=Dj^P9D?n-?iAkZACe;uJ@(P56m)=AM8!!zvjgvxl zHR$#^CQbbsyWZ6_sZ+IGOq;Yf?P71*m^5iOJ9e59L(;O{jF!Kp#PNB|;hf)jTsXFI z7v?iYRVV9KWm~NhiMhnEEOVC-hGG2l6{N4`(nUY;%?-L{TRwOC=x0WjQa$2dj368^ zy@TQ>;)fSM>-p(oF!;fRk7*GW2tWV=5P$##AaD)^4z3D;P+uSWUZAS3mi3*AwQiJk zySWZd7H(MaNVkUEg1cjQdS4tgXtA<4i%5uTk zZ)ejo^^#fJUezmPQO-}z$oZjIc-V=l57nw#Db*_F=aERb5n8M0YE{=t^tZ)0ZC4?g zRow5oqqQrZ%O|sPeubo!6*6RKZsm#Dypo*GIb}o78aqt#N;4nVDScS7{^=~-nr$g!{dmpyd| zTV0OV*6Pz5YdXo@{!nq0Weii(_v{B{`a69|tyL{|yL5t~6gg2O>?izg9v+mMd8pOa zs!krd>wpKgS^9!JG}UdrnLVBa)pUEUV(d7>ca)43V@;fB8*Gs_J&tnXE>g57E2M zbpKj(^;#r)ZEkVKh$Z)?rxwT3qv56X$>P>nVpdw(RkVkx`RdI0dU`2rWRjVDIX*hR znv=)pCl@BI)%dPjH}z;ZI(9QWels#bBDe2EqIbq_wJF-~_BEPvC3Mu3h|`ovYg2Mk zBzOPTM!K*VH|mKkJF}9%cYjt+Tg8dxmHCCek@DW6J-sukr{Xok9(k}nolcIVR8ya{ z>gCfmg=d3IU;jRTNeoR+jUB)zPfi&L7kF_}s(PZ;r>_D*F)kH%KYSy|uGbbW1i zE?i5^SSfQd8m^}oXA@F7Wo?>q>y*uKF7R{W7mRo){y!Ie)jpf7Or(G;=9203C&nz{{NH_zZ0K|f4o8+L{lLE0SG_<0uX=z1Rwwb z2tWV==Ut$e8*nH8jr%LO=4Rs_2j4}m-<|%uHySkV|375J$6~Jc@VvvJ{}6xx1Rwwb z2tWV=5P$##An-Z~EOUP5dRCGW^oaoST$04~hqi2bp5Q^Ob>f7H6W3_G%^wrADJuB< z%s^6-;#;b@VUgL&Mw@!IM)35t-eYO4MM;{gSLrib+SN)Q-$!;DQCh8Dw&;@?+BC0C zQRDjm6Gr@2d?Nn-I*l^=0s#m>00Izz00bZa0SG_<0uXqe1uk*d+14`~?&bfZ6B522 zZa_FXli*zc_lX}d;@9*I3j`nl0SG_<0uX=z1Rwwb2tWV=XHS68+YPQToMdlWx-a=# zo6GzC^wxpPbdObC^Iv_p%?{4|KOoY-|NmS3Q~Xu@QT#^yl5S#w00bZa0SG_<0uX=z z1Rwwb2teSh3v>lImgRiBpA%Rg-$mcKE}jG!nz4$ZT!~y{bKmY;|fB*y_009U<00Izzz}XcD@T@cQ5BPb3PWS!pitBPq>Ez$f zv-DH|=dOG{o)-kcx&F_JkLmOO&TgmBG6+Bb0uX=z1Rwwb2tWV=5P-mW5V*&7GlKLM tBg@%3G1YCI7&b{+nlB%g{sBQXC_n%J literal 0 HcmV?d00001 diff --git a/db_test.py b/db_test.py new file mode 100644 index 000000000..dc5a11b6a --- /dev/null +++ b/db_test.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +""" Populates the database with seed data +""" +from api.db.database import create_database, get_db +from api.v1.models.user import User +from api.v1.models.org import Organisation +from api.v1.models.profile import Profile +from api.v1.models.product import Product + +create_database() +db = next(get_db()) + +user_1 = User(email="test@mail", username="testuser", password="testpass", first_name="John", last_name="Doe") +user_2 = User(email="test1@mail", username="testuser1", password="testpass1", first_name="Jane", last_name="Boyle") +user_3 = User(email="test2@mail", username="testuser2", password="testpass2", first_name="Bob", last_name="Dwayne") + +db.add_all([user_1, user_2, user_3]) + +org_1 = Organisation(name="Python Org", description="An organisation for python develoers") +org_2 = Organisation(name="Django Org", description="An organisation of django devs") +org_3 = Organisation(name="FastAPI Devs", description="An organisation of Fast API devs") + +db.add_all([org_1, org_2, org_3]) + +org_1.users.extend([user_1, user_2, user_3]) +org_2.users.extend([user_1, user_3]) +org_3.users.extend([user_2, user_1]) + +product_1 = Product(name="bed", price=400000) +product_2 = Product(name="shoe", price=150000) + +profile_1 = Profile(bio='My name is John Doe', phone_number='09022112233') +user_1.profile = profile_1 + +db.add_all([product_1, product_2]) +db.commit() +users = db.query(Organisation).first().users +for user in users: + print(user.password) +print(profile_1.user_id) + From 2697e0be151887bccbfa5b89bd2458d3c4fc971b Mon Sep 17 00:00:00 2001 From: Armolas Date: Fri, 19 Jul 2024 00:19:14 +0100 Subject: [PATCH 02/10] applied requested changes --- alembic.ini | 2 +- alembic/env.py | 7 +++- ...s.py => 5a2dc9cc9735_initial_migration.py} | 42 +++++++++---------- .../ba7a518767c3_initial_migration.py | 30 +++++++++++++ api/db/database.py | 8 ++-- api/v1/models/base.py | 7 ++-- api/v1/models/org.py | 16 +++---- api/v1/models/product.py | 4 +- api/v1/models/profile.py | 6 ++- api/v1/models/user.py | 13 +++--- db_test.py | 10 ++--- 11 files changed, 95 insertions(+), 50 deletions(-) rename alembic/versions/{9670771a11f9_initial_migrations.py => 5a2dc9cc9735_initial_migration.py} (69%) create mode 100644 alembic/versions/ba7a518767c3_initial_migration.py diff --git a/alembic.ini b/alembic.ini index ae1dc8be9..484de57fd 100644 --- a/alembic.ini +++ b/alembic.ini @@ -60,7 +60,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = sqlite:///./database.db +sqlalchemy.url = [post_write_hooks] diff --git a/alembic/env.py b/alembic/env.py index b98bf4f22..0caf842ca 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -2,8 +2,9 @@ from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context +from decouple import config as decouple_config from api.v1.models.user import User -from api.v1.models.org import Organisation +from api.v1.models.org import Organization from api.v1.models.profile import Profile from api.v1.models.product import Product from api.v1.models.base import Base @@ -17,6 +18,10 @@ if config.config_file_name is not None: fileConfig(config.config_file_name) +database_url = decouple_config('DATABASE_URL') + +# Set the SQLAlchemy URL dynamically +config.set_main_option('sqlalchemy.url', database_url) # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel diff --git a/alembic/versions/9670771a11f9_initial_migrations.py b/alembic/versions/5a2dc9cc9735_initial_migration.py similarity index 69% rename from alembic/versions/9670771a11f9_initial_migrations.py rename to alembic/versions/5a2dc9cc9735_initial_migration.py index 644c854c6..37de63911 100644 --- a/alembic/versions/9670771a11f9_initial_migrations.py +++ b/alembic/versions/5a2dc9cc9735_initial_migration.py @@ -1,8 +1,8 @@ -"""Initial migrations +"""initial migration -Revision ID: 9670771a11f9 +Revision ID: 5a2dc9cc9735 Revises: -Create Date: 2024-07-18 20:08:23.466008 +Create Date: 2024-07-19 00:08:59.055052 """ from typing import Sequence, Union @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. -revision: str = '9670771a11f9' +revision: str = '5a2dc9cc9735' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -20,17 +20,17 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('organisations', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + op.create_table('organizations', + sa.Column('id', sa.UUID(), nullable=False), sa.Column('name', sa.String(length=50), nullable=False), sa.Column('description', sa.Text(), nullable=True), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('name') ) op.create_table('products', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('id', sa.UUID(), nullable=False), sa.Column('name', sa.String(), nullable=False), sa.Column('description', sa.Text(), nullable=True), sa.Column('price', sa.Numeric(), nullable=False), @@ -38,21 +38,21 @@ def upgrade() -> None: sa.PrimaryKeyConstraint('id') ) op.create_table('users', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('id', sa.UUID(), nullable=False), sa.Column('username', sa.String(length=50), nullable=False), sa.Column('email', sa.String(length=100), nullable=False), sa.Column('password', sa.String(length=255), nullable=False), sa.Column('first_name', sa.String(length=50), nullable=True), sa.Column('last_name', sa.String(length=50), nullable=True), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('email'), sa.UniqueConstraint('username') ) op.create_table('profiles', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), sa.Column('bio', sa.Text(), nullable=True), sa.Column('phone_number', sa.String(length=50), nullable=True), sa.Column('avatar_url', sa.String(length=100), nullable=True), @@ -61,21 +61,21 @@ def upgrade() -> None: sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_table('user_organisation', - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('organisation_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['organisation_id'], ['organisations.id'], ), + op.create_table('user_organization', + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('user_id', 'organisation_id') + sa.PrimaryKeyConstraint('user_id', 'organization_id') ) # ### end Alembic commands ### def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user_organisation') + op.drop_table('user_organization') op.drop_table('profiles') op.drop_table('users') op.drop_table('products') - op.drop_table('organisations') + op.drop_table('organizations') # ### end Alembic commands ### diff --git a/alembic/versions/ba7a518767c3_initial_migration.py b/alembic/versions/ba7a518767c3_initial_migration.py new file mode 100644 index 000000000..25812cb5f --- /dev/null +++ b/alembic/versions/ba7a518767c3_initial_migration.py @@ -0,0 +1,30 @@ +"""initial migration + +Revision ID: ba7a518767c3 +Revises: 5a2dc9cc9735 +Create Date: 2024-07-19 00:15:15.551494 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ba7a518767c3' +down_revision: Union[str, None] = '5a2dc9cc9735' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/api/db/database.py b/api/db/database.py index d53709632..acb90c6f7 100644 --- a/api/db/database.py +++ b/api/db/database.py @@ -11,12 +11,12 @@ def get_db_engine(): - DB_TYPE = config("DB_TYPE", "") - DB_NAME = config("DB_NAME", "") + DB_TYPE = config("DB_TYPE", "postgresql") + DB_NAME = config("DB_NAME", "hng_fast_api") DB_USER = config("DB_USER", "") DB_PASSWORD = config("DB_PASSWORD", "") - DB_HOST = config("DB_HOST", "") - DB_PORT = config("DB_PORT", "") + DB_HOST = config("DB_HOST", "localhost") + DB_PORT = config("DB_PORT", "5432") MYSQL_DRIVER = config("MYSQL_DRIVER", "") DATABASE_URL = "" diff --git a/api/v1/models/base.py b/api/v1/models/base.py index cf75a1544..92c0327ee 100644 --- a/api/v1/models/base.py +++ b/api/v1/models/base.py @@ -10,11 +10,12 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from datetime import datetime +from sqlalchemy.dialects.postgresql import UUID Base = declarative_base() -user_organisation_association = Table('user_organisation', Base.metadata, - Column('user_id', Integer, ForeignKey('users.id'), primary_key=True), - Column('organisation_id', Integer, ForeignKey('organisations.id'), primary_key=True) +user_organization_association = Table('user_organization', Base.metadata, + Column('user_id', UUID(as_uuid=True), ForeignKey('users.id'), primary_key=True), + Column('organization_id', UUID(as_uuid=True), ForeignKey('organizations.id'), primary_key=True) ) diff --git a/api/v1/models/org.py b/api/v1/models/org.py index 1d3554f75..6178427ef 100644 --- a/api/v1/models/org.py +++ b/api/v1/models/org.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -""" The Organisation model +""" The Organization model """ from sqlalchemy import ( Column, @@ -14,13 +14,15 @@ ) from sqlalchemy.orm import relationship from datetime import datetime -from api.v1.models.base import Base, user_organisation_association +from api.v1.models.base import Base, user_organization_association +from sqlalchemy.dialects.postgresql import UUID +from uuid_extensions import uuid7 -class Organisation(Base): - __tablename__ = 'organisations' +class Organization(Base): + __tablename__ = 'organizations' - id = Column(Integer, primary_key=True, autoincrement=True) + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid7) name = Column(String(50), unique=True, nullable=False) description = Column(Text, nullable=True) created_at = Column(DateTime, server_default=func.now()) @@ -28,8 +30,8 @@ class Organisation(Base): users = relationship( "User", - secondary=user_organisation_association, - back_populates="organisations" + secondary=user_organization_association, + back_populates="organizations" ) def __str__(self): diff --git a/api/v1/models/product.py b/api/v1/models/product.py index 3b7e8509e..b3a09490f 100644 --- a/api/v1/models/product.py +++ b/api/v1/models/product.py @@ -12,12 +12,14 @@ ) from datetime import datetime from api.v1.models.base import Base +from sqlalchemy.dialects.postgresql import UUID +from uuid_extensions import uuid7 class Product(Base): __tablename__ = 'products' - id = Column(Integer, primary_key=True, autoincrement=True) + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid7) name = Column(String, nullable=False) description = Column(Text, nullable=True) price = Column(Numeric, nullable=False) diff --git a/api/v1/models/profile.py b/api/v1/models/profile.py index ed9bb94ad..0a780546d 100644 --- a/api/v1/models/profile.py +++ b/api/v1/models/profile.py @@ -12,15 +12,17 @@ func, ) from sqlalchemy.orm import relationship +from sqlalchemy.dialects.postgresql import UUID from datetime import datetime from api.v1.models.base import Base +from uuid_extensions import uuid7 class Profile(Base): __tablename__ = 'profiles' - id = Column(Integer, primary_key=True, autoincrement=True) - user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid7) + user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False) bio = Column(Text, nullable=True) phone_number = Column(String(50), nullable=True) avatar_url = Column(String(100), nullable=True) diff --git a/api/v1/models/user.py b/api/v1/models/user.py index 81d45c080..2d47ff881 100644 --- a/api/v1/models/user.py +++ b/api/v1/models/user.py @@ -16,8 +16,11 @@ ) from sqlalchemy.orm import relationship from datetime import datetime -from api.v1.models.base import Base, user_organisation_association +from api.v1.models.base import Base, user_organization_association import bcrypt +from uuid_extensions import uuid7 +from sqlalchemy.dialects.postgresql import UUID + def hash_password(password: str) -> bytes: """ Hashes the user password for security @@ -30,7 +33,7 @@ def hash_password(password: str) -> bytes: class User(Base): __tablename__ = 'users' - id = Column(Integer, primary_key=True, autoincrement=True) + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid7) username = Column(String(50), unique=True, nullable=False) email = Column(String(100), unique=True, nullable=False) password = Column(String(255), nullable=False) @@ -40,9 +43,9 @@ class User(Base): updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) profile = relationship("Profile", uselist=False) - organisations = relationship( - "Organisation", - secondary=user_organisation_association, + organizations = relationship( + "Organization", + secondary=user_organization_association, back_populates="users" ) diff --git a/db_test.py b/db_test.py index dc5a11b6a..75773dace 100644 --- a/db_test.py +++ b/db_test.py @@ -3,7 +3,7 @@ """ from api.db.database import create_database, get_db from api.v1.models.user import User -from api.v1.models.org import Organisation +from api.v1.models.org import Organization from api.v1.models.profile import Profile from api.v1.models.product import Product @@ -16,9 +16,9 @@ db.add_all([user_1, user_2, user_3]) -org_1 = Organisation(name="Python Org", description="An organisation for python develoers") -org_2 = Organisation(name="Django Org", description="An organisation of django devs") -org_3 = Organisation(name="FastAPI Devs", description="An organisation of Fast API devs") +org_1 = Organization(name="Python Org", description="An organization for python develoers") +org_2 = Organization(name="Django Org", description="An organization of django devs") +org_3 = Organization(name="FastAPI Devs", description="An organization of Fast API devs") db.add_all([org_1, org_2, org_3]) @@ -34,7 +34,7 @@ db.add_all([product_1, product_2]) db.commit() -users = db.query(Organisation).first().users +users = db.query(Organization).first().users for user in users: print(user.password) print(profile_1.user_id) From 4a9c42cc591b7760d24b613677c66e102b099fdd Mon Sep 17 00:00:00 2001 From: Armolas Date: Fri, 19 Jul 2024 07:01:51 +0100 Subject: [PATCH 03/10] Made all Datetime timezone aware --- ...01926f167f_updated_profile_user_columns.py | 30 ++++++ .../versions/e36c5525f3b6_updated_columns.py | 96 +++++++++++++++++++ api/v1/models/org.py | 4 +- api/v1/models/product.py | 3 +- api/v1/models/profile.py | 8 +- api/v1/models/user.py | 6 +- db_test.py | 1 + 7 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 alembic/versions/9c01926f167f_updated_profile_user_columns.py create mode 100644 alembic/versions/e36c5525f3b6_updated_columns.py diff --git a/alembic/versions/9c01926f167f_updated_profile_user_columns.py b/alembic/versions/9c01926f167f_updated_profile_user_columns.py new file mode 100644 index 000000000..970675d5f --- /dev/null +++ b/alembic/versions/9c01926f167f_updated_profile_user_columns.py @@ -0,0 +1,30 @@ +"""updated profile user columns + +Revision ID: 9c01926f167f +Revises: e36c5525f3b6 +Create Date: 2024-07-19 06:56:07.545775 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '9c01926f167f' +down_revision: Union[str, None] = 'e36c5525f3b6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/alembic/versions/e36c5525f3b6_updated_columns.py b/alembic/versions/e36c5525f3b6_updated_columns.py new file mode 100644 index 000000000..b8d29399b --- /dev/null +++ b/alembic/versions/e36c5525f3b6_updated_columns.py @@ -0,0 +1,96 @@ +"""updated columns + +Revision ID: e36c5525f3b6 +Revises: ba7a518767c3 +Create Date: 2024-07-19 06:49:15.962971 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'e36c5525f3b6' +down_revision: Union[str, None] = 'ba7a518767c3' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('organizations', 'created_at', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=True, + existing_server_default=sa.text('now()')) + op.alter_column('organizations', 'updated_at', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=True, + existing_server_default=sa.text('now()')) + op.add_column('products', sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True)) + op.alter_column('products', 'created_at', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=False) + op.alter_column('profiles', 'created_at', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=True) + op.alter_column('profiles', 'updated_at', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=True) + op.create_unique_constraint(None, 'profiles', ['user_id']) + op.alter_column('users', 'created_at', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=True, + existing_server_default=sa.text('now()')) + op.alter_column('users', 'updated_at', + existing_type=postgresql.TIMESTAMP(), + type_=sa.DateTime(timezone=True), + existing_nullable=True, + existing_server_default=sa.text('now()')) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('users', 'updated_at', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=True, + existing_server_default=sa.text('now()')) + op.alter_column('users', 'created_at', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=True, + existing_server_default=sa.text('now()')) + op.drop_constraint(None, 'profiles', type_='unique') + op.alter_column('profiles', 'updated_at', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=True) + op.alter_column('profiles', 'created_at', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=True) + op.alter_column('products', 'created_at', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=False) + op.drop_column('products', 'updated_at') + op.alter_column('organizations', 'updated_at', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=True, + existing_server_default=sa.text('now()')) + op.alter_column('organizations', 'created_at', + existing_type=sa.DateTime(timezone=True), + type_=postgresql.TIMESTAMP(), + existing_nullable=True, + existing_server_default=sa.text('now()')) + # ### end Alembic commands ### diff --git a/api/v1/models/org.py b/api/v1/models/org.py index 6178427ef..d8fea3d89 100644 --- a/api/v1/models/org.py +++ b/api/v1/models/org.py @@ -25,8 +25,8 @@ class Organization(Base): id = Column(UUID(as_uuid=True), primary_key=True, default=uuid7) name = Column(String(50), unique=True, nullable=False) description = Column(Text, nullable=True) - created_at = Column(DateTime, server_default=func.now()) - updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) users = relationship( "User", diff --git a/api/v1/models/product.py b/api/v1/models/product.py index b3a09490f..9692b931f 100644 --- a/api/v1/models/product.py +++ b/api/v1/models/product.py @@ -23,4 +23,5 @@ class Product(Base): name = Column(String, nullable=False) description = Column(Text, nullable=True) price = Column(Numeric, nullable=False) - created_at = Column(DateTime, nullable=False, default=func.now()) + created_at = Column(DateTime(timezone=True), nullable=False, default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) diff --git a/api/v1/models/profile.py b/api/v1/models/profile.py index 0a780546d..6aa3a8b63 100644 --- a/api/v1/models/profile.py +++ b/api/v1/models/profile.py @@ -22,11 +22,11 @@ class Profile(Base): __tablename__ = 'profiles' id = Column(UUID(as_uuid=True), primary_key=True, default=uuid7) - user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), nullable=False) + user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'), unique=True, nullable=False) bio = Column(Text, nullable=True) phone_number = Column(String(50), nullable=True) avatar_url = Column(String(100), nullable=True) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - #user = relationship("User", back_populates="profile") + user = relationship("User", back_populates="profile") diff --git a/api/v1/models/user.py b/api/v1/models/user.py index 2d47ff881..e92791640 100644 --- a/api/v1/models/user.py +++ b/api/v1/models/user.py @@ -39,10 +39,10 @@ class User(Base): password = Column(String(255), nullable=False) first_name = Column(String(50)) last_name = Column(String(50)) - created_at = Column(DateTime, server_default=func.now()) - updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) - profile = relationship("Profile", uselist=False) + profile = relationship("Profile", uselist=False, back_populates="user") organizations = relationship( "Organization", secondary=user_organization_association, diff --git a/db_test.py b/db_test.py index 75773dace..ea1174249 100644 --- a/db_test.py +++ b/db_test.py @@ -38,4 +38,5 @@ for user in users: print(user.password) print(profile_1.user_id) +print(profile_1.user) From b35aded35740fa6c65a2dff6b5b20bbe4ae62aaa Mon Sep 17 00:00:00 2001 From: Armolas <110924174+Armolas@users.noreply.github.com> Date: Fri, 19 Jul 2024 10:45:55 +0100 Subject: [PATCH 04/10] Updated README.md I have updated the README file to include guidline for setting up the database, creating dummy data and also using the database in the routes files Signed-off-by: Armolas <110924174+Armolas@users.noreply.github.com> --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/README.md b/README.md index 27935829a..d7e99e94c 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,63 @@ FastAPI boilerplate python main.py ``` +## **DATABASE TEST SETUP** + +To set up the database, follow the following steps: + +**Cloning** +- clone the repository using `git clone https://github.com/hngprojects/hng_boilerplate_python_fastapi_web` +- `cd` into the directory hng_boilerplate_python_fastapi_web +- switch branch using `git checkout Muritadhor` + +**Environment Setup** +- run `pip install -r requrements.txt` to install dependencies +- create a `.env` file in the root directory and copy the content of `.env.sample` and update it accordingly + +**Create your local database** +```bash +sudo -u root psql +``` +```sql +CREATE USER user WITH PASSWORD 'password'; +CREATE DATABASE hng_fast_api; +GRANT ALL PRIVILEGES ON DATABASE hng_fast_api TO user; +``` + +**Migrate the database** +```bash +alembic revision --autogenerate -m "Initial migrate" +alembic upgrade head +``` + +**create dummy data** +```bash +python3 db_test.py +``` +This should run without any errors + +**Using the database in your route files:** + +make sure to add the following to your file + +```python +from api.db.database import create_database, get_db +from api.v1.models.user import User +from api.v1.models.org import Organization +from api.v1.models.profile import Profile +from api.v1.models.product import Product + +create_database() +db = next(get_db()) +``` +Then use the db for your queries + +example +```python +db.add(User(email="test@mail", username="testuser", password="testpass", first_name="John", last_name="Doe")) +``` + + +**Adding tables and columns to models** + +After creating new tables, or adding new models. Make sure to run alembic revision --autogenerate -m "Migration messge" From f7e95b90fc9490e1ed61f9b7aa050fb72430a8b4 Mon Sep 17 00:00:00 2001 From: Armolas Date: Fri, 19 Jul 2024 10:47:06 +0100 Subject: [PATCH 05/10] updated requirement file --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 853b6833e..7326d8865 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ python-dotenv bcrypt passlib pymysql +uuid7 From 33f4c5d8f04fbf58f4787130c11d4683bd5a345b Mon Sep 17 00:00:00 2001 From: Emmanuel Nwanochie Date: Fri, 19 Jul 2024 21:57:33 +0100 Subject: [PATCH 06/10] added github workflow files and added postgres python dependency --- .github/workflows/cd.dev.yml | 36 +++++++++++++++++++++++ .github/workflows/ci.yml | 57 ++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 94 insertions(+) create mode 100644 .github/workflows/cd.dev.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/cd.dev.yml b/.github/workflows/cd.dev.yml new file mode 100644 index 000000000..02d856842 --- /dev/null +++ b/.github/workflows/cd.dev.yml @@ -0,0 +1,36 @@ +name: Main Branch Deployment + +on: + workflow_run: + workflows: ["CI"] + types: + - completed + branches: [dev] + +jobs: + on-success: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Use SSH Action + uses: appleboy/ssh-action@v0.1.8 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + password: ${{ secrets.PASSWORD }} + script: | + cd python/dev_source_code/ + git pull origin dev + source .venv/bin/activate + pip install -r requirements.txt + + + + on-failure: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + steps: + - run: echo 'The triggering workflow failed' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..56648706a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: [main, staging, dev] + pull_request: + branches: [main, staging, dev] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: + POSTGRES_PASSWORD: + POSTGRES_DB: + ports: + - 5432:5432 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.9" + virtual-environment: venv + + - name: Install dependencies + run: | + pip install -r requirements.txt + + - name: Copy env file for dev + if: github.ref == 'refs/heads/dev' + run: cp .env.development .env + + - name: Copy env file for main + if: github.ref == 'refs/heads/main' + run: cp .env.production .env + + - name: Copy env file for staging + if: github.ref == 'refs/heads/staging' + run: cp .env.staging .env + + # - name: Run migrations + # run: | + # activate + # alembic upgrade head + + # - name: Run tests + # run: | + # activate + # pytest diff --git a/requirements.txt b/requirements.txt index 7326d8865..3b4bb4a4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ bcrypt passlib pymysql uuid7 +psycopg2-binary \ No newline at end of file From 98acd0b0020cab913882c792362bc3d78709a5fd Mon Sep 17 00:00:00 2001 From: Emmanuel Nwanochie Date: Fri, 19 Jul 2024 22:01:06 +0100 Subject: [PATCH 07/10] updated sample env and workflow ci file --- .env.sample | 8 ++++---- .github/workflows/ci.yml | 17 ++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.env.sample b/.env.sample index 300e463bd..7c09ba660 100644 --- a/.env.sample +++ b/.env.sample @@ -1,12 +1,12 @@ PYTHON_ENV=dev -DB_TYPE=mysql +DB_TYPE=postgresql DB_NAME=dbname DB_USER=user DB_PASSWORD=password DB_HOST=127.0.0.1 -DB_PORT=3306 -MYSQL_DRIVER=pymysql -DB_URL="mysql+pymysql://user:password@127.0.0.1:3306/dbname" +DB_PORT=5432 +MYSQL_DRIVER=pymysql +DB_URL="postgresql://user:password@127.0.0.1:5432/dbname" SECRET_KEY = "" ALGORITHM = HS256 ACCESS_TOKEN_EXPIRE_MINUTES = 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56648706a..92bc6fb43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,17 +34,16 @@ jobs: run: | pip install -r requirements.txt - - name: Copy env file for dev - if: github.ref == 'refs/heads/dev' - run: cp .env.development .env + - name: Copy env file + run: cp .env.sample .env - - name: Copy env file for main - if: github.ref == 'refs/heads/main' - run: cp .env.production .env + # - name: Copy env file for main + # if: github.ref == 'refs/heads/main' + # run: cp .env.production .env - - name: Copy env file for staging - if: github.ref == 'refs/heads/staging' - run: cp .env.staging .env + # - name: Copy env file for staging + # if: github.ref == 'refs/heads/staging' + # run: cp .env.staging .env # - name: Run migrations # run: | From fc509f29d4ce2f74cb18679279a33f6f3a350744 Mon Sep 17 00:00:00 2001 From: Emmanuel Nwanochie Date: Fri, 19 Jul 2024 22:14:01 +0100 Subject: [PATCH 08/10] added staging and prod workflow files --- .github/workflows/cd.dev.yml | 4 ++-- .github/workflows/cd.prod.yml | 36 ++++++++++++++++++++++++++++++++ .github/workflows/cd.staging.yml | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/cd.prod.yml create mode 100644 .github/workflows/cd.staging.yml diff --git a/.github/workflows/cd.dev.yml b/.github/workflows/cd.dev.yml index 02d856842..74bd6a44e 100644 --- a/.github/workflows/cd.dev.yml +++ b/.github/workflows/cd.dev.yml @@ -1,4 +1,4 @@ -name: Main Branch Deployment +name: Dev Branch Deployment on: workflow_run: @@ -26,7 +26,7 @@ jobs: git pull origin dev source .venv/bin/activate pip install -r requirements.txt - + on-failure: diff --git a/.github/workflows/cd.prod.yml b/.github/workflows/cd.prod.yml new file mode 100644 index 000000000..21a26eba3 --- /dev/null +++ b/.github/workflows/cd.prod.yml @@ -0,0 +1,36 @@ +name: Dev Branch Deployment + +on: + workflow_run: + workflows: ["CI"] + types: + - completed + branches: [main] + +jobs: + on-success: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Use SSH Action + uses: appleboy/ssh-action@v0.1.8 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + password: ${{ secrets.PASSWORD }} + script: | + cd python/prod_source_code/ + git pull origin main + source .venv/bin/activate + pip install -r requirements.txt + + + + on-failure: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + steps: + - run: echo 'The triggering workflow failed' diff --git a/.github/workflows/cd.staging.yml b/.github/workflows/cd.staging.yml new file mode 100644 index 000000000..01653a2a7 --- /dev/null +++ b/.github/workflows/cd.staging.yml @@ -0,0 +1,36 @@ +name: Dev Branch Deployment + +on: + workflow_run: + workflows: ["CI"] + types: + - completed + branches: [staging] + +jobs: + on-success: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Use SSH Action + uses: appleboy/ssh-action@v0.1.8 + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + password: ${{ secrets.PASSWORD }} + script: | + cd python/staging_source_code/ + git pull origin staging + source .venv/bin/activate + pip install -r requirements.txt + + + + on-failure: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + steps: + - run: echo 'The triggering workflow failed' From 7289067faf52c6c2d0b9e4c4d5f89b39598bd1aa Mon Sep 17 00:00:00 2001 From: Emmanuel Nwanochie Date: Fri, 19 Jul 2024 22:19:20 +0100 Subject: [PATCH 09/10] testing pipeline falure --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92bc6fb43..f2d1503c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,10 +32,10 @@ jobs: - name: Install dependencies run: | - pip install -r requirements.txt + pip install -r requirgdements.txt - - name: Copy env file - run: cp .env.sample .env + # - name: Copy env file + # run: cp .env.sample .env # - name: Copy env file for main # if: github.ref == 'refs/heads/main' From 2fc1eadbb757b9b5c243ccc50bc9e832376e6d8e Mon Sep 17 00:00:00 2001 From: Emmanuel Nwanochie Date: Fri, 19 Jul 2024 22:20:33 +0100 Subject: [PATCH 10/10] pipeline success --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2d1503c7..c36e7b435 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - name: Install dependencies run: | - pip install -r requirgdements.txt + pip install -r requirements.txt # - name: Copy env file # run: cp .env.sample .env