Merge remote-tracking branch 'origin/backend' into main
This commit is contained in:
commit
696e288817
10 changed files with 482 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
duckpond.db
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
```
|
|
@ -1 +0,0 @@
|
|||
__version__ = '0.1.0'
|
20
backend/duckpond/__main__.py
Normal file
20
backend/duckpond/__main__.py
Normal 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
90
backend/duckpond/db.py
Normal 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()
|
66
backend/duckpond/endpoints.py
Normal file
66
backend/duckpond/endpoints.py
Normal 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()
|
||||
})
|
4
backend/duckpond/paths.py
Normal file
4
backend/duckpond/paths.py
Normal 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
241
backend/poetry.lock
generated
Normal 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 = []
|
|
@ -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
1
backend/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
flask
|
Reference in a new issue