diff options
author | 2018-03-06 20:05:44 +0100 | |
---|---|---|
committer | 2018-03-06 19:05:44 +0000 | |
commit | 5d685b27c5454e29809fe6039e0cf8945cbbb52f (patch) | |
tree | 2782bd8144f744bfc46924f64fb57f465cf31d67 /pysite | |
parent | Fix 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.py | 3 | ||||
-rw-r--r-- | pysite/database.py | 30 | ||||
-rw-r--r-- | pysite/decorators.py | 1 | ||||
-rw-r--r-- | pysite/route_manager.py | 2 | ||||
-rw-r--r-- | pysite/views/api/bot/tag.py | 57 | ||||
-rw-r--r-- | pysite/views/api/bot/tags.py | 116 | ||||
-rw-r--r-- | pysite/views/api/error_view.py | 40 |
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 |