diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..09b0929 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,51 @@ +name: Build + +on: + push: + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: "3.10" + + - name: Install Dependencies + run: | + pip install -r requirements.txt + + - name: Test + env: + DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/postgres + run: | + python -m pytest + + - name: Export DB on Failure + if: ${{ failure() }} + env: + DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/postgres + run: | + pip install 'db-to-sqlite[postgresql]' + db-to-sqlite "$DATABASE_URL" postgres.db --all + + - name: Run Stoat Action + uses: stoat-dev/stoat-action@v0 + if: always() + diff --git a/.gitignore b/.gitignore index 6704566..68bc17f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,104 +1,160 @@ -# Logs -logs +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.stoat/comment.hbs b/.stoat/comment.hbs new file mode 100644 index 0000000..b19c21b --- /dev/null +++ b/.stoat/comment.hbs @@ -0,0 +1,5 @@ +{{#if plugins.static_hosting.postgres_dump.link}} +❌ [View Database](https://lite.datasette.io/?url={{ plugins.static_hosting.postgres_dump.link }}) +{{else}} +✅ All Tests Passed +{{/if}} diff --git a/.stoat/config.yaml b/.stoat/config.yaml new file mode 100644 index 0000000..574edd0 --- /dev/null +++ b/.stoat/config.yaml @@ -0,0 +1,10 @@ +--- +version: 1 +enabled: true +comment_template_file: ".stoat/comment.hbs" +plugins: + job_runtime: + enabled: true + static_hosting: + postgres_dump: + path: postgres.db diff --git a/README.md b/README.md index d1ef5dc..dce8884 100644 --- a/README.md +++ b/README.md @@ -1 +1,13 @@ -# example-datasette \ No newline at end of file +# example-datasette + +``` +docker run --name example-datasette-db -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres -d postgres +``` + +``` +python3 -m venv env +pip install -r requirements.txt +source env/bin/activate +export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres +python -m pytest +``` diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..689a8cb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +psycopg2-binary == 2.9.5 +pytest == 7.2.1 diff --git a/test_db.py b/test_db.py new file mode 100644 index 0000000..0ebf0f9 --- /dev/null +++ b/test_db.py @@ -0,0 +1,50 @@ +import os +import pytest +import psycopg2 + +@pytest.fixture(scope="module") +def test_db(): + conn = psycopg2.connect(os.environ.get('DATABASE_URL')) + yield conn + conn.close() + +# define a test function to test the database content +def test_database_content(test_db): + # create a cursor object to interact with the database + cur = test_db.cursor() + + # create table + cur.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL)"); + cur.execute("CREATE TABLE IF NOT EXISTS companies (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL)"); + cur.execute("TRUNCATE users, companies"); + + # ensure that the users table is empty + cur.execute("SELECT COUNT(*) FROM users") + assert cur.fetchone()[0] == 0 + + # insert new users + cur.execute("INSERT INTO users (name, email) VALUES (%s, %s)", ("John", "johndoe@example.com")) + cur.execute("INSERT INTO users (name, email) VALUES (%s, %s)", ("Jared", "jared@example.com")) + cur.execute("INSERT INTO users (name, email) VALUES (%s, %s)", ("Liren", "liren@example.com")) + test_db.commit() + + # insert new companies + cur.execute("INSERT INTO companies (name) VALUES (%s)", ("Google",)) + cur.execute("INSERT INTO companies (name) VALUES (%s)", ("Facebook",)) + test_db.commit() + + # retrieve all users and validate that the new user is in the table + cur.execute("SELECT COUNT(*) FROM users WHERE name = %s AND email = %s", ("John", "johndoe@example.com")) + assert cur.fetchone()[0] == 1 + + # force failure so we can test stoat functionality of reading the dumped db + assert false + + # delete the user and ensure that the users table is empty again + cur.execute("DELETE FROM users WHERE name = %s", ("John",)) + test_db.commit() + cur.execute("SELECT COUNT(*) FROM users") + assert cur.fetchone()[0] == 0 + + # close the cursor + cur.close()