Merge remote-tracking branch 'origin/backend' into main

This commit is contained in:
Lewie Hill 2022-10-29 18:53:48 +01:00
commit 696e288817
10 changed files with 482 additions and 2 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
duckpond.db
### Python ###
# Byte-compiled / optimized / DLL files

View file

@ -1 +1,58 @@
# Duck Pond backend
# Duck Pond backend
## Endpoints
### GET `/entries`
Gets a list of all the duck ponds in the database.
### POST `/entries/<id>/new`
Creates a new duck pond entry.
JSON body arguments:
* `name` - reqiured
* `location` - required, in the form
```json
{
"lat": -1.2345,
"long": 33.56643
}
```
* `imageURL`
Votes will be initialised as zero.
Return sample:
```json
{
"id": "uuid"
}
```
### PATCH `/entries/<id>`
Updates an entry with the ID `id`.
JSON body arguments:
* `name`
* `location`
* `imageURL`
* `votes`
### GET `/entries/<id>`
Gets the JSON of the pond.
```json
{
"id": "uuid",
"name": "The Pondiest Pond",
"location": {
"lat": -1.2345,
"long": 33.56643
},
"imageURL": "https://example.com"
}
```

View file

@ -1 +0,0 @@
__version__ = '0.1.0'

View file

@ -0,0 +1,20 @@
import flask
import db
import endpoints
__version__ = '0.1.0'
def main():
app = flask.Flask(__name__)
database = db.DB("duckpond.db")
_ = endpoints.Endpoints(app, database)
app.run(port=8080, debug=True, host="127.0.0.1")
if __name__ == "__main__":
main()

90
backend/duckpond/db.py Normal file
View file

@ -0,0 +1,90 @@
import sqlite3
from dataclasses import dataclass
from typing import *
import urllib
@dataclass
class Entry:
id: str
name: str
location_lat: float
location_long: float
votes: int
image_url: Optional[str]
def validate(self, only_populated_fields=False) -> Tuple[bool, str]:
if (self.name == "" or self.name is None) and not only_populated_fields:
return False, "name cannot be empty"
if self.votes < 0:
return False, "votes cannot be negative"
if self.image_url is not None:
try:
urllib.parse.urlparse(self.image_url)
except Exception:
return False, "invalid URL"
if self.location_lat is None or self.location_long is None:
return False, "missing locations"
return True, ""
def as_dict(self) -> Dict:
res = {}
res["id"] = self.id
res["name"] = self.name
res["location"] = {
"lat": self.location_lat,
"long": self.location_long,
}
res["votes"] = self.votes
if self.image_url is not None:
res["imageURL"] = self.image_url
return res
class DB:
conn: sqlite3.Connection
def __init__(self, filename: str):
self.conn = sqlite3.connect(filename)
def _setup(self, filename: str):
cursor = self.conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS entries
([ID] INTEGER PRIMARY KEY, [title] TEXT, [latitude] FLOAT, [longitude] FLOAT, [votes] INTEGER, [image_url], STRING)
''')
cursor.commit()
def getEntries(self):
cursor = self.conn.cursor()
cursor.execute('SELECT * FROM entries')
myresult = cursor.fetchall()
for x in myresult:
print(x)
def addEntry(self, title, latitude, longitude, votes, image_url):
insertArray = [title, latitude, longitude, votes, image_url]
cursor = self.conn.cursor()
cursor.execute('INSERT INTO entries (title, latitude, longitude, votes, image_url) VALUES (?, ?, ?, ?, ?);', insertArray)
cursor.commit()
def deleteEntry(self, ID):
cursor = self.conn.cursor()
cursor.execute('DELETE FROM entries WHERE ID = ?', ID)
cursor.commit()
def updateEntry(self, ID, title, latitude, longitude, votes, image_url):
updateArray = [ID, title, latitude, longitude, votes, image_url]
cursor = self.conn.cursor()
cursor.execute('UPDATE entries SET title = ?, latitude = ?, longitude = ?, votes = ?, image_url = ?;', updateArray)
cursor.commit()

View file

@ -0,0 +1,66 @@
from typing import *
import uuid
import flask
import db
import paths
class Endpoints:
db: db.DB
def __init__(self, app: flask.Flask, database: db.DB):
self.db = database
app.add_url_rule(paths.ENTRIES, view_func=self.list_entries, methods=["GET"])
app.add_url_rule(paths.GET_ENTRY, view_func=self.get_entry, methods=["GET"])
app.add_url_rule(paths.UPDATE_ENTRY, view_func=self.update_entry, methods=["PATCH"])
app.add_url_rule(paths.CREATE_ENTRY, view_func=self.create_entry, methods=["POST"])
def list_entries(self):
# TODO: populate from databaase
a = db.Entry("203fc6a0-9587-41a4-9862-e1b72039b98b", "Birmingham Duck Pond", -1.2345, 33.4567, 0, None)
b = db.Entry("b140e048-ea2c-4827-b670-ef41ba48c56d", "Northwich Duck Pond", -3.2345, 25.4567, 0, None)
return flask.jsonify([a.as_dict(), b.as_dict()])
def get_entry(self, entry_id: str):
# TODO: Fetch from database
return flask.jsonify({
"id": entry_id,
"TODO": "TODO"
})
def update_entry(self):
return "", 204
def create_entry(self):
body = flask.request.get_json()
if body is None:
return "no JSON body", 400
coordinates = body.get("location", None)
if coordinates is None:
return "missing location", 400
new_entry = db.Entry(
uuid.uuid4(),
body.get("name"),
coordinates.get("lat"),
coordinates.get("long"),
0,
body.get("imageURL"))
validation_result, error_text = new_entry.validate()
if not validation_result:
return flask.abort(400, error_text)
# TODO: store in database
# TODO: Form responses
return flask.jsonify({
"id": uuid.uuidv4()
})

View file

@ -0,0 +1,4 @@
ENTRIES = "/entries"
GET_ENTRY = "/entry/<entry_id>"
UPDATE_ENTRY = "/entry/<entry_id>"
CREATE_ENTRY = "/entry"

241
backend/poetry.lock generated Normal file
View file

@ -0,0 +1,241 @@
[[package]]
name = "atomicwrites"
version = "1.4.1"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "22.1.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
[[package]]
name = "flask"
version = "2.2.2"
description = "A simple framework for building complex web applications."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
click = ">=8.0"
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
itsdangerous = ">=2.0"
Jinja2 = ">=3.0"
Werkzeug = ">=2.2.2"
[package.extras]
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]
[[package]]
name = "importlib-metadata"
version = "5.0.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
[[package]]
name = "itsdangerous"
version = "2.1.2"
description = "Safely pass data to untrusted environments and back."
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "markupsafe"
version = "2.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "more-itertools"
version = "9.0.0"
description = "More routines for operating on iterables, beyond itertools"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "pluggy"
version = "0.13.1"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["railroad-diagrams", "jinja2"]
[[package]]
name = "pytest"
version = "5.4.3"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=17.4.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
more-itertools = ">=4.0.0"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.5.0"
wcwidth = "*"
[package.extras]
checkqa-mypy = ["mypy (==v0.761)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
name = "wcwidth"
version = "0.2.5"
description = "Measures the displayed width of unicode strings in a terminal"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "werkzeug"
version = "2.2.2"
description = "The comprehensive WSGI web application library."
category = "main"
optional = false
python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog"]
[[package]]
name = "zipp"
version = "3.10.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "6662025d53c721988eccf3e4101ee5d3686702f80c82f900e6f4d087282743a1"
[metadata.files]
atomicwrites = []
attrs = []
click = []
colorama = []
flask = []
importlib-metadata = []
itsdangerous = []
jinja2 = []
markupsafe = []
more-itertools = []
packaging = []
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
py = []
pyparsing = []
pytest = []
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
werkzeug = []
zipp = []

View file

@ -6,6 +6,7 @@ authors = ["AKP <abi@tdpain.net>"]
[tool.poetry.dependencies]
python = "^3.8"
Flask = "^2.2.2"
[tool.poetry.dev-dependencies]
pytest = "^5.2"

1
backend/requirements.txt Normal file
View file

@ -0,0 +1 @@
flask