Skip to content
Snippets Groups Projects
Commit ed97e2fd authored by Adrian Block's avatar Adrian Block
Browse files

Merge branch 'feat/postgresql-storage' into 'dev'

 added database storage and broker

See merge request paulo/paulo-backend!1
parents fc5ab438 7c1dd067
No related branches found
No related tags found
1 merge request!1:sparkles: added database storage and broker
Showing
with 456 additions and 12 deletions
......@@ -3,3 +3,8 @@ SERVER_HOST=http://localhost
BACKEND_CORS_ORIGINS='["http://localhost"]'
PROJECT_NAME=PAULO
POSTGRES_SERVER=localhost
POSTGRES_USER=paulo
POSTGRES_PASSWORD=paulo
POSTGRES_DB=paulo
......@@ -3,7 +3,6 @@ venv/
__pycache__/
.vscode/
data.json
data/
data*.json
.env
FROM python:3.9
WORKDIR /backend
COPY . /backend
RUN pip3 install poetry
RUN poetry config virtualenvs.create false
RUN poetry install --no-dev
RUN pip install uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration files
# file_template = %%(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.
# string value is passed to dateutil.tz.gettz()
# 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
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[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
# 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
Generic single-database configuration.
\ No newline at end of file
import os
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
from dotenv import load_dotenv
load_dotenv()
# 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
from app.data.storage import models
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():
user = os.getenv("POSTGRES_USER", "postgres")
password = os.getenv("POSTGRES_PASSWORD", "")
server = os.getenv("POSTGRES_SERVER", "db")
db = os.getenv("POSTGRES_DB", "app")
return f"postgresql://{user}:{password}@{server}/{db}"
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()
"""${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"}
"""init
Revision ID: 1ce4e17617c1
Revises:
Create Date: 2022-01-11 00:31:35.355535
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1ce4e17617c1'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('appointment',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('start_time', sa.DateTime(), nullable=False),
sa.Column('end_time', sa.DateTime(), nullable=False),
sa.Column('room', sa.VARCHAR(length=64), nullable=False),
sa.Column('instructors', sa.VARCHAR(length=128), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('semester',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.VARCHAR(length=32), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('course',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('cid', sa.VARCHAR(length=32), nullable=False),
sa.Column('name', sa.VARCHAR(length=64), nullable=False),
sa.Column('description', sa.VARCHAR(length=255), nullable=True),
sa.Column('semester_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['semester_id'], ['semester.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('course_appointment',
sa.Column('course_id', sa.Integer(), nullable=False),
sa.Column('appointment_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['appointment_id'], ['appointment.id'], ),
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
sa.PrimaryKeyConstraint('course_id', 'appointment_id')
)
op.create_table('small_group',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.VARCHAR(length=64), nullable=False),
sa.Column('course_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('small_group_appointment',
sa.Column('small_group_id', sa.Integer(), nullable=False),
sa.Column('appointment_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['appointment_id'], ['appointment.id'], ),
sa.ForeignKeyConstraint(['small_group_id'], ['small_group.id'], ),
sa.PrimaryKeyConstraint('small_group_id', 'appointment_id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('small_group_appointment')
op.drop_table('small_group')
op.drop_table('course_appointment')
op.drop_table('course')
op.drop_table('semester')
op.drop_table('appointment')
# ### end Alembic commands ###
"""search index
Revision ID: 278aca658ba6
Revises: 1ce4e17617c1
Create Date: 2022-01-12 10:41:34.182181
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '278aca658ba6'
down_revision = '1ce4e17617c1'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
"""overflow fix
Revision ID: bed1aefe514f
Revises: 278aca658ba6
Create Date: 2022-01-26 11:52:48.248018
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'bed1aefe514f'
down_revision = '278aca658ba6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('appointment', 'room',
existing_type=sa.VARCHAR(length=64),
type_=sa.VARCHAR(length=128),
existing_nullable=False)
op.alter_column('appointment', 'instructors',
existing_type=sa.VARCHAR(length=128),
type_=sa.VARCHAR(length=256),
existing_nullable=False)
op.alter_column('course', 'name',
existing_type=sa.VARCHAR(length=64),
type_=sa.VARCHAR(length=256),
existing_nullable=False)
op.alter_column('small_group', 'name',
existing_type=sa.VARCHAR(length=64),
type_=sa.VARCHAR(length=128),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('small_group', 'name',
existing_type=sa.VARCHAR(length=128),
type_=sa.VARCHAR(length=64),
existing_nullable=False)
op.alter_column('course', 'name',
existing_type=sa.VARCHAR(length=256),
type_=sa.VARCHAR(length=64),
existing_nullable=False)
op.alter_column('appointment', 'instructors',
existing_type=sa.VARCHAR(length=256),
type_=sa.VARCHAR(length=128),
existing_nullable=False)
op.alter_column('appointment', 'room',
existing_type=sa.VARCHAR(length=128),
type_=sa.VARCHAR(length=64),
existing_nullable=False)
# ### end Alembic commands ###
......@@ -8,6 +8,7 @@ app = typer.Typer()
app.add_typer(cli.scraper_command, name="scraper")
app.add_typer(cli.courses_command, name="courses")
app.add_typer(cli.db_command, name="db")
if __name__ == "__main__":
app()
from .scraper import scraper_command
from .courses import courses_command
from .db import db_command
import asyncio
import typer
from app import scraper
from app.data.brokers import ListBroker
from app.data.storage import PlainJSONStorage
......@@ -9,14 +7,14 @@ courses_command = typer.Typer()
@courses_command.command()
def find(name: str):
def find(semester: str, name: str):
pjs = PlainJSONStorage()
pjs.init_data()
broker = ListBroker(pjs)
courses = broker.find_courses_by_name(name)
courses = broker.find_courses_by_name(semester, name)
for c in courses:
print("--------------")
print("name:", c.name)
print("id:", c.id)
print("id:", c.cid)
print("--------------")
import asyncio
import typer
from app import scraper, schemas
from app.database import get_session
from app.processing import mapper
db_command = typer.Typer()
@db_command.command()
def scrapesite(semester: str, site_path: str):
session = next(get_session())
courses = asyncio.run(scraper.find_and_parse_courses(site_path))
s = schemas.Semester(name=semester, courses=courses)
db_s = mapper.map_semester(s)
session.add(db_s)
session.commit()
print("Success!")
import asyncio
import typer
import schemas
from app import scraper
from app.data.storage import PlainJSONStorage
......@@ -25,11 +27,14 @@ def catalogue(
def courses(url: str = None, interactive: bool = typer.Option(True, "--no-interactive"),
verbose: bool = typer.Option(False, "--verbose", "-v")):
if not url:
site_path = list(scraper.parse_semesters("https://paul.uni-paderborn.de/scripts"
scrape_result = scraper.parse_semesters("https://paul.uni-paderborn.de/scripts"
"/mgrqispi.dll?APPNAME=CampusNet&PRGNAME=EXTERNALPAGES"
"&ARGUMENTS=-N000000000000001,-N000442,-Avvz").values())[0]
"&ARGUMENTS=-N000000000000001,-N000442,-Avvz")
semester_name = list(scrape_result.keys())[0]
site_path = scrape_result[semester_name]
else:
site_path = url.replace("https://paul.uni-paderborn.de", "")
semester_name = "Custom"
course_list = scraper.parse_courses_on_site(site_path)
if interactive:
......@@ -56,5 +61,5 @@ def courses(url: str = None, interactive: bool = typer.Option(True, "--no-intera
if verbose:
print(data)
PlainJSONStorage.save_data(data)
PlainJSONStorage.save_data([schemas.Semester(name=semester_name, courses=data)])
print("Saved to plain JSON storage.")
from .api import api_settings
from .database import get_database_settings
......@@ -21,6 +21,8 @@ class APISettings(BaseSettings):
PROJECT_NAME: str = 'PAULO'
BROKER: str = 'database'
class Config:
case_sensitive = True
env_file = '.env'
......
from typing import Optional, Dict, Any
from pydantic import BaseSettings, validator, PostgresDsn
from pydantic.tools import lru_cache
class DatabaseSettings(BaseSettings):
POSTGRES_SERVER: str
POSTGRES_USER: str
POSTGRES_PASSWORD: str
POSTGRES_DB: str
SQLALCHEMY_DATABASE_URI: Optional[str] = None
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
if isinstance(v, str):
return v
return PostgresDsn.build(
scheme="postgresql",
user=values.get("POSTGRES_USER"),
password=values.get("POSTGRES_PASSWORD"),
host=values.get("POSTGRES_SERVER"),
path=f"/{values.get('POSTGRES_DB') or ''}",
)
class Config:
env_file = ".env"
env_file_encoding = 'utf-8'
@lru_cache
def get_database_settings() -> DatabaseSettings:
return DatabaseSettings()
from .broker import Broker
from .list import ListBroker
from .database import DatabaseBroker
from .broker import Broker
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment