aboutsummaryrefslogtreecommitdiffstats
path: root/pysite
diff options
context:
space:
mode:
authorGravatar Leon Sandøy <[email protected]>2018-03-06 20:05:44 +0100
committerGravatar Jeremiah Boby <[email protected]>2018-03-06 19:05:44 +0000
commit5d685b27c5454e29809fe6039e0cf8945cbbb52f (patch)
tree2782bd8144f744bfc46924f64fb57f465cf31d67 /pysite
parentFix user API test (diff)
API for tags (#34)
* Help page and misc improvements Committing so I can go home >:| * [WIP] - API improvements for the tag features. Not completed. * renaming tag.py to tags.py and refactoring the nomenclature of docs to tags * fixed error message in tags, cleaning up app_test.py * tests for the tags feature * ignoring jsonify returns cause coverall can't handle them * Catch-all error view for the API blueprint * cleaning up APIErrorView a little * bringing coverage for tags.py to 100% * how did this get in here? * how did this get in here? ROUND 2 * Removing the 503 database error handling. It's not in use and we should probably rethink that whole custom error handling system anyway. * Converting the tags file to use the @api_params decorator instead of validating manually. Tested with bot staging.
Diffstat (limited to 'pysite')
-rw-r--r--pysite/base_route.py3
-rw-r--r--pysite/database.py30
-rw-r--r--pysite/decorators.py1
-rw-r--r--pysite/route_manager.py2
-rw-r--r--pysite/views/api/bot/tag.py57
-rw-r--r--pysite/views/api/bot/tags.py116
-rw-r--r--pysite/views/api/error_view.py40
7 files changed, 189 insertions, 60 deletions
diff --git a/pysite/base_route.py b/pysite/base_route.py
index f389b56e..4e1a63a7 100644
--- a/pysite/base_route.py
+++ b/pysite/base_route.py
@@ -86,11 +86,12 @@ class APIView(RouteView):
... return self.error(ErrorCodes.unknown_route)
"""
- def error(self, error_code: ErrorCodes) -> Response:
+ def error(self, error_code: ErrorCodes, error_info: str = "") -> Response:
"""
Generate a JSON response for you to return from your handler, for a specific type of API error
:param error_code: The type of error to generate a response for - see `constants.ErrorCodes` for more
+ :param error_info: An optional message with more information about the error.
:return: A Flask Response object that you can return from your handler
"""
diff --git a/pysite/database.py b/pysite/database.py
index 78c4368a..add76923 100644
--- a/pysite/database.py
+++ b/pysite/database.py
@@ -103,6 +103,34 @@ class RethinkDB:
self.log.debug(f"Table created: '{table_name}'")
return True
+ def delete(self, table_name: str, primary_key: Optional[str] = None,
+ durability: str = "hard", return_changes: Union[bool, str] = False
+ ) -> Union[Dict[str, Any], None]:
+ """
+ Delete one or all documents from a table. This can only delete
+ either the contents of an entire table, or a single document.
+ For more complex delete operations, please use self.query.
+
+ :param table_name: The name of the table to delete from. This must be provided.
+ :param primary_key: The primary_key to delete from that table. This is optional.
+ :param durability: "hard" (the default) to write the change immediately, "soft" otherwise
+ :param return_changes: Whether to return a list of changed values or not - defaults to False
+ :return: if return_changes is True, returns a dict containing all changes. Else, returns None.
+ """
+
+ if primary_key:
+ query = self.query(table_name).get(primary_key).delete(
+ durability=durability, return_changes=return_changes
+ )
+ else:
+ query = self.query(table_name).delete(
+ durability=durability, return_changes=return_changes
+ )
+
+ if return_changes:
+ return self.run(query, coerce=dict)
+ self.run(query)
+
def drop_table(self, table_name: str):
"""
Attempt to drop a table from the database, along with its data
@@ -168,7 +196,7 @@ class RethinkDB:
:param connect_database: If creating a new connection, whether to connect to the database immediately
:param coerce: Optionally, an object type to attempt to coerce the result to
- :return: THe result of the operation
+ :return: The result of the operation
"""
if not new_connection:
diff --git a/pysite/decorators.py b/pysite/decorators.py
index 952c2349..d8bf1381 100644
--- a/pysite/decorators.py
+++ b/pysite/decorators.py
@@ -48,6 +48,7 @@ def api_params(schema: Schema, validation_type: ValidationTypes = ValidationType
if not isinstance(data, list):
data = [data]
+
except JSONDecodeError:
return self.error(ErrorCodes.bad_data_format) # pragma: no cover
diff --git a/pysite/route_manager.py b/pysite/route_manager.py
index 53b24def..72517a3c 100644
--- a/pysite/route_manager.py
+++ b/pysite/route_manager.py
@@ -27,7 +27,7 @@ class RouteManager:
self.db = RethinkDB()
self.log = logging.getLogger()
self.app.secret_key = os.environ.get("WEBPAGE_SECRET_KEY", "super_secret")
- self.app.config["SERVER_NAME"] = os.environ.get("SERVER_NAME", "pythondiscord.com:8080")
+ self.app.config["SERVER_NAME"] = os.environ.get("SERVER_NAME", "pythondiscord.local:8080")
self.app.before_request(self.db.before_request)
self.app.teardown_request(self.db.teardown_request)
diff --git a/pysite/views/api/bot/tag.py b/pysite/views/api/bot/tag.py
deleted file mode 100644
index 8818074e..00000000
--- a/pysite/views/api/bot/tag.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# coding=utf-8
-
-from flask import jsonify, request
-
-from pysite.base_route import APIView
-from pysite.constants import ErrorCodes
-from pysite.decorators import api_key
-from pysite.mixins import DBMixin
-
-
-class TagView(APIView, DBMixin):
- path = "/tag"
- name = "tag"
- table_name = "tag"
- table_primary_key = "tag_name"
-
- @api_key
- def get(self):
- """
- Data must be provided as params,
- API key must be provided as header
- """
-
- tag_name = request.args.get("tag_name")
-
- if tag_name:
- data = self.db.get(self.table_name, tag_name) or {} # pragma: no cover
- else:
- data = self.db.pluck(self.table_name, "tag_name") or []
-
- return jsonify(data) # pragma: no cover
-
- @api_key
- def post(self):
- """
- Data must be provided as JSON.
- """
-
- data = request.get_json()
-
- tag_name = data.get("tag_name")
- tag_content = data.get("tag_content")
- tag_category = data.get("tag_category")
-
- if tag_name and tag_content:
- self.db.insert(
- self.table_name,
- {
- "tag_name": tag_name,
- "tag_content": tag_content,
- "tag_category": tag_category
- }
- )
- else:
- return self.error(ErrorCodes.incorrect_parameters)
-
- return jsonify({"success": True}) # pragma: no cover
diff --git a/pysite/views/api/bot/tags.py b/pysite/views/api/bot/tags.py
new file mode 100644
index 00000000..9db926f4
--- /dev/null
+++ b/pysite/views/api/bot/tags.py
@@ -0,0 +1,116 @@
+# coding=utf-8
+
+from flask import jsonify
+from schema import Schema, Optional
+
+from pysite.base_route import APIView
+from pysite.constants import ValidationTypes
+from pysite.decorators import api_key, api_params
+from pysite.mixins import DBMixin
+
+GET_SCHEMA = Schema([
+ {
+ Optional("tag_name"): str
+ }
+])
+
+POST_SCHEMA = Schema([
+ {
+ "tag_name": str,
+ "tag_content": str
+ }
+])
+
+DELETE_SCHEMA = Schema([
+ {
+ "tag_name": str
+ }
+])
+
+
+class TagsView(APIView, DBMixin):
+ path = "/tags"
+ name = "tags"
+ table_name = "tags"
+ table_primary_key = "tag_name"
+
+ @api_key
+ @api_params(schema=GET_SCHEMA, validation_type=ValidationTypes.params)
+ def get(self, params=None):
+ """
+ Fetches tags from the database.
+
+ - If tag_name is provided, it fetches
+ that specific tag.
+
+ - If tag_category is provided, it fetches
+ all tags in that category.
+
+ - If nothing is provided, it will
+ fetch a list of all tag_names.
+
+ Data must be provided as params.
+ API key must be provided as header.
+ """
+
+ tag_name = None
+
+ if params:
+ tag_name = params[0].get("tag_name")
+
+ if tag_name:
+ data = self.db.get(self.table_name, tag_name) or {}
+ else:
+ data = self.db.pluck(self.table_name, "tag_name") or []
+
+ return jsonify(data)
+
+ @api_key
+ @api_params(schema=POST_SCHEMA, validation_type=ValidationTypes.json)
+ def post(self, json_data):
+ """
+ If the tag_name doesn't exist, this
+ saves a new tag in the database.
+
+ If the tag_name already exists,
+ this will edit the existing tag.
+
+ Data must be provided as JSON.
+ API key must be provided as header.
+ """
+
+ json_data = json_data[0]
+
+ tag_name = json_data.get("tag_name")
+ tag_content = json_data.get("tag_content")
+
+ self.db.insert(
+ self.table_name,
+ {
+ "tag_name": tag_name,
+ "tag_content": tag_content
+ },
+ conflict="update" # If it exists, update it.
+ )
+
+ return jsonify({"success": True})
+
+ @api_key
+ @api_params(schema=DELETE_SCHEMA, validation_type=ValidationTypes.json)
+ def delete(self, data):
+ """
+ Deletes a tag from the database.
+
+ Data must be provided as JSON.
+ API key must be provided as header.
+ """
+
+ json = data[0]
+ tag_name = json.get("tag_name")
+
+ self.db.delete(
+ self.table_name,
+ tag_name
+ )
+
+ return jsonify({"success": True})
diff --git a/pysite/views/api/error_view.py b/pysite/views/api/error_view.py
new file mode 100644
index 00000000..e5301336
--- /dev/null
+++ b/pysite/views/api/error_view.py
@@ -0,0 +1,40 @@
+# coding=utf-8
+from flask import jsonify
+from werkzeug.exceptions import HTTPException
+
+from pysite.base_route import ErrorView
+
+
+class APIErrorView(ErrorView):
+ name = "api_error_all"
+ error_code = range(400, 600)
+
+ def __init__(self):
+
+ # Direct errors for all methods at self.return_error
+ methods = [
+ 'get', 'post', 'put',
+ 'delete', 'patch', 'connect',
+ 'options', 'trace'
+ ]
+
+ for method in methods:
+ setattr(self, method, self.return_error)
+
+ def return_error(self, error: HTTPException):
+ """
+ Return a basic JSON object representing the HTTP error,
+ as well as propagating its status code
+ """
+
+ message = str(error)
+ code = 500
+
+ if isinstance(error, HTTPException):
+ message = error.description
+ code = error.code
+
+ return jsonify({
+ "error_code": -1,
+ "error_message": message
+ }), code