Skip to content

Commit

Permalink
Merge pull request #57 from Police-Data-Accessibility-Project/mc_issu…
Browse files Browse the repository at this point in the history
…e_356_set_up_swagger

Mc issue 356 set up swagger
  • Loading branch information
maxachis authored Jul 18, 2024
2 parents fc47768 + 1ae55bb commit de1b696
Show file tree
Hide file tree
Showing 19 changed files with 523 additions and 44 deletions.
122 changes: 89 additions & 33 deletions resources/Agencies.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,110 @@

from flask import Response
from flask_restx import fields

from middleware.agencies import get_agencies
from middleware.security import api_required
from resources.resource_helpers import add_api_key_header_arg
from utilities.namespace import create_namespace
from resources.PsycopgResource import PsycopgResource, handle_exceptions


approved_columns = [
"name",
"homepage_url",
"count_data_sources",
"agency_type",
"multi_agency",
"submitted_name",
"jurisdiction_type",
"state_iso",
"municipality",
"zip_code",
"county_fips",
"county_name",
"lat",
"lng",
"data_sources",
"no_web_presence",
"airtable_agency_last_modified",
"data_sources_last_updated",
"approved",
"rejection_reason",
"last_approval_editor",
"agency_created",
"county_airtable_uid",
"defunct_year",
"airtable_uid",
]
namespace_agencies = create_namespace()

inner_model = namespace_agencies.model(
"Agency Item",
{
"name": fields.String(required=True, description="The name of the agency"),
"homepage_url": fields.String(description="The homepage URL of the agency"),
"count_data_sources": fields.Integer(description="The count of data sources"),
"agency_type": fields.String(description="The type of the agency"),
"multi_agency": fields.Boolean(
description="Indicates if the agency is multi-agency"
),
"submitted_name": fields.String(description="The submitted name of the agency"),
"jurisdiction_type": fields.String(
description="The jurisdiction type of the agency"
),
"state_iso": fields.String(description="The ISO code of the state"),
"municipality": fields.String(description="The municipality of the agency"),
"zip_code": fields.String(description="The ZIP code of the agency"),
"county_fips": fields.String(description="The FIPS code of the county"),
"county_name": fields.String(description="The name of the county"),
"lat": fields.Float(description="The latitude of the agency location"),
"lng": fields.Float(description="The longitude of the agency location"),
"data_sources": fields.String(
description="The data sources related to the agency"
),
"no_web_presence": fields.Boolean(
description="Indicates if the agency has no web presence"
),
"airtable_agency_last_modified": fields.DateTime(
description="The last modified date in Airtable"
),
"data_sources_last_updated": fields.DateTime(
description="The last updated date of data sources"
),
"approved": fields.Boolean(description="Indicates if the agency is approved"),
"rejection_reason": fields.String(
description="The reason for rejection, if any"
),
"last_approval_editor": fields.String(description="The last approval editor"),
"agency_created": fields.DateTime(
description="The creation date of the agency"
),
"county_airtable_uid": fields.String(
description="The Airtable UID of the county"
),
"defunct_year": fields.Integer(
description="The year the agency became defunct"
),
"airtable_uid": fields.String(description="The Airtable UID of the agency"),
},
)

output_model = namespace_agencies.model(
"Agencies Result",
{
'count': fields.Integer(description='Total count of agencies'),
"data": fields.List(fields.Nested(inner_model), description='List of agencies'),
},
)

parser = namespace_agencies.parser()
parser.add_argument(
"page",
type=int,
required=True,
location="args",
help="The page number of results to return.",
default=1,
)
add_api_key_header_arg(parser)

@namespace_agencies.route("/agencies/<page>")
@namespace_agencies.expect(parser)
@namespace_agencies.doc(
description="Get a paginated list of approved agencies from the database.",
responses={
200: "Success. Returns a paginated list of approved agencies.",
500: "Internal server error.",
403: "Unauthorized. Forbidden or an invalid API key.",
400: "Bad request. Missing or bad API key",
},
)
class Agencies(PsycopgResource):
"""Represents a resource for fetching approved agency data from the database."""

@handle_exceptions
@api_required
def get(self, page: str) -> Response:
@namespace_agencies.response(
200,
"Success. Returns a paginated list of approved agencies.",
model=output_model,
)
# @namespace_agencies.marshal_with(output_model)
def get(self, page: int) -> Response:
"""
Retrieves a paginated list of approved agencies from the database.
Parameters:
- page (str): The page number of results to return.
Returns:
- dict: A dictionary containing the count of returned agencies and their data.
"""
Expand Down
27 changes: 27 additions & 0 deletions resources/ApiKey.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
from flask import request, Response
from flask_restx import fields

from middleware.login_queries import get_api_key_for_user
from resources.resource_helpers import create_user_model
from utilities.namespace import create_namespace

from resources.PsycopgResource import PsycopgResource, handle_exceptions

namespace_api_key = create_namespace()

api_key_model = namespace_api_key.model(
"ApiKey",
{
"api_key": fields.String(
required=True,
description="The generated API key",
example="2bd77a1d7ef24a1dad3365b8a5c6994e"
),
},
)

user = create_user_model(namespace_api_key)

@namespace_api_key.route("/api_key")
@namespace_api_key.expect(user)
@namespace_api_key.doc(
description="Generates an API key for authenticated users.",
responses={
200: "Success",
401: "Invalid email or password",
500: "Internal server error.",
},
)
class ApiKey(PsycopgResource):
"""Represents a resource for generating an API key for authenticated users."""

@handle_exceptions
@namespace_api_key.response(200, "Success", model=api_key_model)
def get(self) -> Response:
"""
Authenticates a user based on provided credentials and generates an API key.
Reads the 'email' and 'password' from the JSON body of the request, validates the user,
and if successful, generates and returns a new API key.
If the email and password match a row in the database, a new API key is created using uuid.uuid4().hex, updated in for the matching user in the users table, and the API key is sent to the user.
Returns:
- dict: A dictionary containing the generated API key, or None if an error occurs.
"""
Expand Down
51 changes: 48 additions & 3 deletions resources/Archives.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from flask_restx import fields

from middleware.security import api_required
from middleware.archives_queries import (
archives_get_query,
Expand All @@ -8,10 +10,34 @@
import json
from typing import Dict, Any

from resources.resource_helpers import add_api_key_header_arg
from utilities.namespace import create_namespace
from resources.PsycopgResource import PsycopgResource, handle_exceptions

namespace_archives = create_namespace()
from resources.PsycopgResource import PsycopgResource, handle_exceptions

archives_get_model = namespace_archives.model(
"ArchivesResponse",
{
"id": fields.String(description="The ID of the data source"),
"last_cached": fields.Date(description="The last date the data was cached"),
"source_url": fields.String(description="The URL of the data source"),
"update_frequency": fields.String(description="The archive update frequency of the data source"),
},
)

archives_post_model = namespace_archives.model(
"ArchivesPost",
{
"id": fields.String(description="The ID of the data source", required=True),
"last_cached": fields.Date(description="The last date the data was cached"),
"broken_source_url_as_of": fields.Date(description="The date the source was marked as broken"),
},
)

archives_header_parser = namespace_archives.parser()
add_api_key_header_arg(archives_header_parser)


@namespace_archives.route("/archives")
class Archives(PsycopgResource):
Expand All @@ -21,6 +47,13 @@ class Archives(PsycopgResource):

@handle_exceptions
@api_required
@namespace_archives.response(200, "Success: Returns a list of archived data sources", archives_get_model)
@namespace_archives.response(400, "Error: Bad request missing or bad API key")
@namespace_archives.response(403, "Error: Unauthorized. Forbidden or an invalid API key")
@namespace_archives.doc(
description="Retrieves archived data sources from the database.",
)
@namespace_archives.expect(archives_header_parser)
def get(self) -> Any:
"""
Retrieves archived data sources from the database.
Expand All @@ -39,14 +72,26 @@ def get(self) -> Any:

@handle_exceptions
@api_required
@namespace_archives.doc(
description="Updates the archive data based on the provided JSON payload.",
responses={
200: "Success: Returns a status message indicating success or an error message if an exception occurs.",
400: "Error: Bad request missing or bad API key",
403: "Error: Unauthorized. Forbidden or an invalid API key",
500: "Error: Internal server error",
}
)
@namespace_archives.expect(archives_header_parser, archives_post_model)
def put(self) -> Dict[str, str]:
"""
Updates the archive data based on the provided JSON payload.
Expects a JSON payload with archive data source identifiers and updates them in the database.
Expects a JSON payload with archive data source identifiers and updates them in the database. The put method
on the archives endpoint updates the data source matching the passed id, updating the last_cached date if it
alone is passed, or it and the broken_source_url_as_of field and the url_status to 'broken'.
Returns:
- dict: A status message indicating success or an error message if an exception occurs.
- dict: A status message indicating success or an error message if an exception occurs.
"""
json_data = request.get_json()
data = json.loads(json_data)
Expand Down
Loading

0 comments on commit de1b696

Please sign in to comment.