diff --git a/python-fastapi/.zcloudignore b/python-fastapi/.zcloudignore new file mode 100644 index 0000000..308086b --- /dev/null +++ b/python-fastapi/.zcloudignore @@ -0,0 +1 @@ +app2 \ No newline at end of file diff --git a/python-fastapi/Dockerfile b/python-fastapi/Dockerfile new file mode 100644 index 0000000..69cdfa4 --- /dev/null +++ b/python-fastapi/Dockerfile @@ -0,0 +1,18 @@ +# Example from https://fastapi.tiangolo.com/deployment/docker/ +FROM python:3.10 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app +COPY ./alembic.ini /code/alembic.ini +COPY ./alembic /code/alembic + +COPY ./entrypoint.sh /entrypoint.sh + +RUN chmod +x /entrypoint.sh + +CMD ["/entrypoint.sh"] diff --git a/python-fastapi/README.md b/python-fastapi/README.md new file mode 100644 index 0000000..3a0c845 --- /dev/null +++ b/python-fastapi/README.md @@ -0,0 +1 @@ +# Example based on Fast API Tutorial https://fastapi.tiangolo.com/tutorial/sql-databases/#migrations \ No newline at end of file diff --git a/python-fastapi/alembic.ini b/python-fastapi/alembic.ini new file mode 100755 index 0000000..7f6cb5c --- /dev/null +++ b/python-fastapi/alembic.ini @@ -0,0 +1,36 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +[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/python-fastapi/alembic/README b/python-fastapi/alembic/README new file mode 100755 index 0000000..98e4f9c --- /dev/null +++ b/python-fastapi/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/python-fastapi/alembic/__init__.py b/python-fastapi/alembic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-fastapi/alembic/env.py b/python-fastapi/alembic/env.py new file mode 100755 index 0000000..3fad7f8 --- /dev/null +++ b/python-fastapi/alembic/env.py @@ -0,0 +1,83 @@ +from __future__ import with_statement + +import os + +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +# target_metadata = None + +from app.database import Base # noqa + +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_url(): + return os.getenv("DATABASE_URL", "") + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = get_url() + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + configuration = config.get_section(config.config_ini_section) + configuration["sqlalchemy.url"] = get_url() + connectable = engine_from_config( + configuration, prefix="sqlalchemy.", poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata, compare_type=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/python-fastapi/alembic/script.py.mako b/python-fastapi/alembic/script.py.mako new file mode 100755 index 0000000..2c01563 --- /dev/null +++ b/python-fastapi/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/python-fastapi/alembic/versions/.keep b/python-fastapi/alembic/versions/.keep new file mode 100755 index 0000000..e69de29 diff --git a/python-fastapi/alembic/versions/__init__.py b/python-fastapi/alembic/versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-fastapi/alembic/versions/d4867f3a4c0a_first_revision.py b/python-fastapi/alembic/versions/d4867f3a4c0a_first_revision.py new file mode 100644 index 0000000..a43bf9d --- /dev/null +++ b/python-fastapi/alembic/versions/d4867f3a4c0a_first_revision.py @@ -0,0 +1,59 @@ +"""First revision + +Revision ID: d4867f3a4c0a +Revises: +Create Date: 2019-04-17 13:53:32.978401 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "d4867f3a4c0a" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("full_name", sa.String(), nullable=True), + sa.Column("email", sa.String(), nullable=True), + sa.Column("hashed_password", sa.String(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.Column("is_superuser", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) + op.create_index(op.f("ix_user_full_name"), "user", ["full_name"], unique=False) + op.create_index(op.f("ix_user_id"), "user", ["id"], unique=False) + op.create_table( + "item", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("owner_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["owner_id"], ["user.id"],), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_item_description"), "item", ["description"], unique=False) + op.create_index(op.f("ix_item_id"), "item", ["id"], unique=False) + op.create_index(op.f("ix_item_title"), "item", ["title"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_item_title"), table_name="item") + op.drop_index(op.f("ix_item_id"), table_name="item") + op.drop_index(op.f("ix_item_description"), table_name="item") + op.drop_table("item") + op.drop_index(op.f("ix_user_id"), table_name="user") + op.drop_index(op.f("ix_user_full_name"), table_name="user") + op.drop_index(op.f("ix_user_email"), table_name="user") + op.drop_table("user") + # ### end Alembic commands ### diff --git a/python-fastapi/app/__init__.py b/python-fastapi/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python-fastapi/app/crud.py b/python-fastapi/app/crud.py new file mode 100644 index 0000000..77e32fc --- /dev/null +++ b/python-fastapi/app/crud.py @@ -0,0 +1,37 @@ +from sqlalchemy.orm import Session + +from app import schemas +from app import models + + +def get_user(db: Session, user_id: int): + return db.query(models.User).filter(models.User.id == user_id).first() + + +def get_user_by_email(db: Session, email: str): + return db.query(models.User).filter(models.User.email == email).first() + + +def get_users(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.User).offset(skip).limit(limit).all() + + +def create_user(db: Session, user: schemas.UserCreate): + fake_hashed_password = user.password + "notreallyhashed" + db_user = models.User(email=user.email, hashed_password=fake_hashed_password) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +def get_items(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.Item).offset(skip).limit(limit).all() + + +def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): + db_item = models.Item(**item.dict(), owner_id=user_id) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item diff --git a/python-fastapi/app/database.py b/python-fastapi/app/database.py new file mode 100644 index 0000000..e8cc15c --- /dev/null +++ b/python-fastapi/app/database.py @@ -0,0 +1,14 @@ +import os + +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = os.environ['DATABASE_URL'] + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() diff --git a/python-fastapi/app/main.py b/python-fastapi/app/main.py new file mode 100644 index 0000000..3a4df76 --- /dev/null +++ b/python-fastapi/app/main.py @@ -0,0 +1,53 @@ +from fastapi import Depends, FastAPI, HTTPException +from sqlalchemy.orm import Session + +from . import models, crud, schemas +from .database import SessionLocal, engine + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=list[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user( + user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) +): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/python-fastapi/app/models.py b/python-fastapi/app/models.py new file mode 100644 index 0000000..62d8ab4 --- /dev/null +++ b/python-fastapi/app/models.py @@ -0,0 +1,26 @@ +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) + + items = relationship("Item", back_populates="owner") + + +class Item(Base): + __tablename__ = "items" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + description = Column(String, index=True) + owner_id = Column(Integer, ForeignKey("users.id")) + + owner = relationship("User", back_populates="items") diff --git a/python-fastapi/app/schemas.py b/python-fastapi/app/schemas.py new file mode 100644 index 0000000..aea2e3f --- /dev/null +++ b/python-fastapi/app/schemas.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel + + +class ItemBase(BaseModel): + title: str + description: str | None = None + + +class ItemCreate(ItemBase): + pass + + +class Item(ItemBase): + id: int + owner_id: int + + class Config: + orm_mode = True + + +class UserBase(BaseModel): + email: str + + +class UserCreate(UserBase): + password: str + + +class User(UserBase): + id: int + is_active: bool + items: list[Item] = [] + + class Config: + orm_mode = True diff --git a/python-fastapi/entrypoint.sh b/python-fastapi/entrypoint.sh new file mode 100644 index 0000000..2636640 --- /dev/null +++ b/python-fastapi/entrypoint.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +alembic upgrade head +uvicorn app.main:app --host 0.0.0.0 --port 80 \ No newline at end of file diff --git a/python-fastapi/requirements.txt b/python-fastapi/requirements.txt new file mode 100644 index 0000000..f1d2c35 --- /dev/null +++ b/python-fastapi/requirements.txt @@ -0,0 +1,6 @@ +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +sqlalchemy>=2.0.21 +psycopg2-binary>=2.9.9 +alembic>=1.12.0 \ No newline at end of file