diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..aaab0cb --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,72 @@ +FROM ubuntu:22.04 + +# Setup workspace directory +RUN mkdir /workspace +WORKDIR /workspace + +# Install useful system utilities +ENV TZ=America/New_York +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install --yes \ + apt-transport-https \ + ca-certificates \ + curl \ + debian-keyring \ + debian-archive-keyring \ + git \ + gnupg \ + locales \ + postgresql-client \ + software-properties-common \ + sudo \ + tzdata \ + wget \ + zsh \ + && rm -rf /var/lib/apt/lists/* + +# Install Python 3.11 +RUN add-apt-repository ppa:deadsnakes/ppa \ + && apt update \ + && apt install --yes \ + python3.11 \ + python3-pip \ + libpq-dev \ + python3.11-dev \ + && rm -rf /var/lib/apt/lists* \ + && unlink /usr/bin/python3 \ + && ln -s /usr/bin/python3.11 /usr/bin/python3 + +# Use a non-root user per https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user +ARG USERNAME=vscode +ARG USER_UID=1000 +ARG USER_GID=$USER_UID + +# Add non-root user and add to sudoers +RUN groupadd --gid $USER_GID $USERNAME \ + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /usr/bin/zsh \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME + +# Set code to default git commit editor +RUN git config --system core.editor "code --wait" +# Set Safe Directory +RUN git config --system safe.directory '/workspace' + +# Configure zsh +USER $USERNAME +ENV HOME /home/$USERNAME + +# Add zsh theme with niceties +RUN curl https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh | bash - \ + && sed -i 's/robbyrussell/kennethreitz/g' ~/.zshrc \ + && echo 'source <(ng completion script)' >>~/.zshrc \ + && echo 'export PATH=$PATH:$HOME/.local/bin' >>~/.zshrc + +# Set Locale for Functional Autocompletion in zsh +RUN sudo locale-gen en_US.UTF-8 + +# Install Database Dependencies +COPY backend/requirements.txt /workspace/backend/requirements.txt +WORKDIR /workspace/backend +RUN python3 -m pip install -r requirements.txt \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..793f5d8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +{ + "dockerComposeFile": "docker-compose.yml", + "workspaceFolder": "/workspace", + "service": "httpd", + "remoteUser": "vscode", + "forwardPorts": [ + 5432, + 5050 + ], + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "ecmel.vscode-html-css", + "ms-vscode.vscode-typescript-next", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss", + "vscode-icons-team.vscode-icons", + "tamasfe.even-better-toml", + "ckolkman.vscode-postgres", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.black-formatter", + "gruntfuggly.todo-tree", + "ms-azuretools.vscode-docker" + ], + "settings": { + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.analysis.extraPaths": [ + "/backend/" + ], + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.analysis.diagnosticSeverityOverrides": { + "reportMissingParameterType": "error", + "reportGeneralTypeIssues": "error", + "reportDeprecated": "error", + "reportImportCycles": "error" + } + } + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/.devcontainer/docker-compose.yml similarity index 63% rename from docker-compose.yml rename to .devcontainer/docker-compose.yml index f2cabf8..1503d5b 100644 --- a/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,12 +1,22 @@ version: "3" services: + httpd: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + volumes: + - ..:/workspace + command: /bin/sh -c "while sleep 1000; do :; done" + environment: + - windir # Defined on Windows but not on other platforms + db: image: "postgres:latest" ports: - "5432:5432" env_file: - - ./backend/.env + - ../backend/.env volumes: - compass-center-postgres:/var/lib/postgresql/data # - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql:ro diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 11dae3a..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "recommendations": [ - "dbaeumer.vscode-eslint", - "ecmel.vscode-html-css", - "ms-vscode.vscode-typescript-next", - "esbenp.prettier-vscode", - "bradlc.vscode-tailwindcss", - "vscode-icons-team.vscode-icons", - "tamasfe.even-better-toml", - "ckolkman.vscode-postgres", - "ms-python.python", - "ms-python.vscode-pylance", - "ms-python.black-formatter" - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 3cf96ac..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "editor.formatOnSave": true, - "editor.formatOnSaveMode": "file", - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[python]": { - "editor.defaultFormatter": "ms-python.autopep8" - }, - "python.analysis.extraPaths": ["backend/"], - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, - "python.analysis.diagnosticSeverityOverrides": { - "reportMissingParameterType": "error", - "reportGeneralTypeIssues": "error", - "reportDeprecated": "error", - "reportImportCycles": "error" - }, - "python.analysis.autoImportCompletions": false, - "python.analysis.typeCheckingMode": "off" -} diff --git a/backend/entities/sample_entity.py b/backend/entities/sample_entity.py index 28948ff..4759694 100644 --- a/backend/entities/sample_entity.py +++ b/backend/entities/sample_entity.py @@ -4,9 +4,9 @@ from .entity_base import EntityBase class SampleEntity(EntityBase): - __tablename__ = 'persons' + __tablename__ = "persons" - id: Mapped[int] = mapped_column(Integer, primary_key=True) + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String, nullable=False) age: Mapped[int] = mapped_column(Integer) email: Mapped[str] = mapped_column(String, unique=True, nullable=False) diff --git a/backend/script/create_database.py b/backend/script/create_database.py new file mode 100644 index 0000000..3d2f17a --- /dev/null +++ b/backend/script/create_database.py @@ -0,0 +1,14 @@ +from sqlalchemy import text, create_engine +from ..database import engine, _engine_str +from ..env import getenv + +engine = create_engine(_engine_str(), echo=True) +"""Application-level SQLAlchemy database engine.""" + +with engine.connect() as connection: + connection.execute( + text("COMMIT") + ) + database = getenv("POSTGRES_DATABASE") + stmt = text(f"CREATE DATABASE {database}") + connection.execute(stmt) \ No newline at end of file diff --git a/backend/script/delete_database.py b/backend/script/delete_database.py new file mode 100644 index 0000000..0679da1 --- /dev/null +++ b/backend/script/delete_database.py @@ -0,0 +1,14 @@ +from sqlalchemy import text, create_engine +from ..database import engine, _engine_str +from ..env import getenv + +engine = create_engine(_engine_str(), echo=True) +"""Application-level SQLAlchemy database engine.""" + +with engine.connect() as connection: + connection.execute( + text("COMMIT") + ) + database = getenv("POSTGRES_DATABASE") + stmt = text(f"DROP DATABASE IF EXISTS {database}") + connection.execute(stmt) \ No newline at end of file diff --git a/backend/script/reset_demo.py b/backend/script/reset_demo.py index 65de426..da06da8 100644 --- a/backend/script/reset_demo.py +++ b/backend/script/reset_demo.py @@ -1,38 +1,18 @@ -from sqlalchemy import text, create_engine -from ..database import engine +from sqlalchemy import create_engine +import subprocess + +from ..database import engine, _engine_str from ..env import getenv from .. import entities database = getenv("POSTGRES_DATABASE") - -def _engine_str() -> str: - """Helper function for reading settings from environment variables to produce connection string.""" - dialect = "postgresql+psycopg2" - user = getenv("POSTGRES_USER") - password = getenv("POSTGRES_PASSWORD") - host = getenv("POSTGRES_HOST") - port = getenv("POSTGRES_PORT") - return f"{dialect}://{user}:{password}@{host}:{port}" - - engine = create_engine(_engine_str(), echo=True) """Application-level SQLAlchemy database engine.""" - -with engine.connect() as connection: - connection.execute( - text("COMMIT") - ) - database = getenv("POSTGRES_DATABASE") - stmt = text(f"DROP DATABASE IF EXISTS {database}") - connection.execute(stmt) - connection.execute( - text("COMMIT") - ) - database = getenv("POSTGRES_DATABASE") - stmt = text(f"CREATE DATABASE {database}") - connection.execute(stmt) +# Run Delete and Create Database Scripts +subprocess.run(["python3", "-m", "backend.script.delete_database"]) +subprocess.run(["python3", "-m", "backend.script.create_database"]) entities.EntityBase.metadata.drop_all(engine) entities.EntityBase.metadata.create_all(engine) diff --git a/backend/test/services/conftest.py b/backend/test/services/conftest.py index 42b2fdd..5b2a984 100644 --- a/backend/test/services/conftest.py +++ b/backend/test/services/conftest.py @@ -1,22 +1,48 @@ """Shared pytest fixtures for database dependent tests.""" import pytest -from sqlalchemy import Engine +from sqlalchemy import Engine, create_engine, text from sqlalchemy.orm import Session -import subprocess +from sqlalchemy.exc import OperationalError -from ...database import db_session +from ...database import _engine_str +from ...env import getenv +from ... import entities + +POSTGRES_DATABASE = f'{getenv("POSTGRES_DATABASE")}_test' +POSTGRES_USER = getenv("POSTGRES_USER") + +def reset_database(): + engine = create_engine(_engine_str(database="")) + with engine.connect() as connection: + try: + conn = connection.execution_options(autocommit=False) + conn.execute(text("ROLLBACK")) # Get out of transactional mode... + conn.execute(text(f"DROP DATABASE IF EXISTS {POSTGRES_DATABASE}")) + except OperationalError: + print( + "Could not drop database because it's being accessed by others (psql open?)" + ) + exit(1) + + conn.execute(text(f"CREATE DATABASE {POSTGRES_DATABASE}")) + conn.execute( + text( + f"GRANT ALL PRIVILEGES ON DATABASE {POSTGRES_DATABASE} TO {POSTGRES_USER}" + ) + ) @pytest.fixture(scope="session") def test_engine() -> Engine: - subprocess.run(["python3", "-m", "backend.script.create_database"]) - session = db_session() - return session + reset_database() + return create_engine(_engine_str(POSTGRES_DATABASE)) @pytest.fixture(scope="function") def session(test_engine: Engine): + entities.EntityBase.metadata.drop_all(test_engine) + entities.EntityBase.metadata.create_all(test_engine) session = Session(test_engine) try: yield session diff --git a/backend/test/services/sample_test.py b/backend/test/services/sample_test.py index ad2167e..f21423e 100644 --- a/backend/test/services/sample_test.py +++ b/backend/test/services/sample_test.py @@ -1,13 +1,21 @@ """Sample Test File""" -import pytest -from sqlalchemy import Engine +from sqlalchemy import Engine, select + +from ... import entities +from ...entities.sample_entity import SampleEntity -def test_sample(session: Engine): - print(session) - assert session != None +def test_entity_count(): + """Checks the number of entities to be inserted""" + print(entities.EntityBase.metadata.tables.keys()) + assert len(entities.EntityBase.metadata.tables.keys()) == 1 -def test_tables(session: Engine): - print() +def test_add_sample_data(session: Engine): + """Inserts a sample data point and verifies it is in the database""" + entity = SampleEntity(name="Praj", age=19, email="pmoha@unc.edu") + session.add(entity) + session.commit() + data = session.get(SampleEntity, 1) + assert data.name == "Praj"