From 058d0c9a194a45cd49925989c34f3d35fac3beca Mon Sep 17 00:00:00 2001 From: Will Usher Date: Tue, 10 Feb 2026 13:28:39 +0000 Subject: [PATCH 1/3] Trigger build to test deployment credentials --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6bff8ee..2537f97 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Once the VM is up and running, SSH into the VM, download and install memgraph curl -O https://download.memgraph.com/memgraph/v2.14.1/ubuntu-20.04/memgraph_2.14.1-1_amd64.deb sudo dpkg -i /memgraph_2.14.1-1_amd64.deb + Set up the Memgraph user to match the credentials specified in the `.env` file. ### 2. Build Docker container From ff52e36ebd4fbb5ff90f4433e4c9ba4489c95b25 Mon Sep 17 00:00:00 2001 From: Will Usher Date: Tue, 10 Feb 2026 13:31:57 +0000 Subject: [PATCH 2/3] Add pre commit config --- .pre-commit-config.yaml | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3906c84 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +exclude: '^docs/conf.py' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-ast + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: mixed-line-ending + args: ['--fix=auto'] # replace 'auto' with 'lf' to enforce Linux/Mac line endings or 'crlf' for Windows + +- repo: https://github.com/pycqa/isort + rev: 7.0.0 + hooks: + - id: isort + args: ["--profile", "black", "--filter-files"] + +- repo: https://github.com/psf/black + rev: 26.1.0 + hooks: + - id: black + language_version: python3 + +- repo: https://github.com/PyCQA/flake8 + rev: 7.3.0 + hooks: + - id: flake8 + ## You can add flake8 plugins via `additional_dependencies`: + # additional_dependencies: [flake8-bugbear] + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.19.1 # Use the sha / tag you want to point at + hooks: + - id: mypy + additional_dependencies: ['types-requests'] From 5f75e2ad6e6ccb389b0b3e3e5eab7f1f246eece5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:32:12 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .dockerignore | 2 +- .../development_dev-research-index.yml | 2 +- .github/workflows/main_research-index.yml | 2 +- .gitignore | 2 +- Dockerfile | 2 +- app/api/author.py | 36 ++-- app/api/country.py | 33 ++-- app/api/output.py | 26 +-- app/api/workstream.py | 25 ++- app/core/config.py | 1 + app/crud/author.py | 56 +++--- app/crud/country.py | 60 +++---- app/crud/ingest.py | 3 +- app/crud/output.py | 85 ++++----- app/crud/workstream.py | 56 +++--- app/db/session.py | 4 +- app/main.py | 170 ++++++++---------- app/schemas/__init__.py | 10 +- app/schemas/affiliation.py | 3 +- app/schemas/author.py | 10 +- app/schemas/country.py | 7 +- app/schemas/meta.py | 4 + app/schemas/output.py | 11 +- app/schemas/query.py | 17 +- app/schemas/topic.py | 5 +- app/schemas/workstream.py | 7 +- app/static/js/map.js | 2 +- app/static/js/network.js | 2 +- app/static/js/world.js | 1 - app/templates/author.html | 2 +- app/templates/author_list.html | 2 +- app/templates/authors.html | 2 +- app/templates/base.html | 2 +- app/templates/country.html | 2 +- app/templates/country_list.html | 2 +- app/templates/header.html | 2 +- app/templates/output.html | 2 +- app/templates/output_list.j2 | 2 +- app/templates/output_popup.html | 2 +- app/templates/outputs.html | 2 +- app/templates/pagination.html | 2 +- app/templates/workstream.html | 2 +- app/templates/workstream_list.html | 2 +- app/test_html.py | 7 +- app/test_main.py | 45 +++-- authors.ttl | 1 - compose.yaml | 2 +- requirements.txt | 4 +- 48 files changed, 359 insertions(+), 372 deletions(-) diff --git a/.dockerignore b/.dockerignore index ea7f2ce..5b94f46 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ .git* **/*.pyc -.venv/ \ No newline at end of file +.venv/ diff --git a/.github/workflows/development_dev-research-index.yml b/.github/workflows/development_dev-research-index.yml index 0bd1b93..4426da1 100644 --- a/.github/workflows/development_dev-research-index.yml +++ b/.github/workflows/development_dev-research-index.yml @@ -49,4 +49,4 @@ jobs: app-name: 'dev-research-index' slot-name: 'production' publish-profile: ${{ secrets.AzureAppService_PublishProfile_a74f0a22a9c3465a91fa629137af0f81 }} - images: 'index.docker.io/${{ secrets.AzureAppService_ContainerUsername_9310ce663981441ab232b6b875b913e2 }}/ccg-research-index:${{ github.sha }}' \ No newline at end of file + images: 'index.docker.io/${{ secrets.AzureAppService_ContainerUsername_9310ce663981441ab232b6b875b913e2 }}/ccg-research-index:${{ github.sha }}' diff --git a/.github/workflows/main_research-index.yml b/.github/workflows/main_research-index.yml index 0d37ab4..1133906 100644 --- a/.github/workflows/main_research-index.yml +++ b/.github/workflows/main_research-index.yml @@ -49,4 +49,4 @@ jobs: app-name: 'research-index' slot-name: 'production' publish-profile: ${{ secrets.AzureAppService_PublishProfile_4b15095a06cc4e57aa33f534f4e54311 }} - images: 'index.docker.io/${{ secrets.AzureAppService_ContainerUsername_dce678b65c414723a5c407473fe6c430 }}/ccg-research-index:${{ github.sha }}' \ No newline at end of file + images: 'index.docker.io/${{ secrets.AzureAppService_ContainerUsername_dce678b65c414723a5c407473fe6c430 }}/ccg-research-index:${{ github.sha }}' diff --git a/.gitignore b/.gitignore index caacd68..c7f0722 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,4 @@ build/ research_index_backend/ data/ -*.bash \ No newline at end of file +*.bash diff --git a/Dockerfile b/Dockerfile index 1d37fed..7cf42ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,4 @@ EXPOSE 8000 CMD ["fastapi", "run", "app/main.py", \ "--port", "8000", \ "--workers", "4", \ - "--proxy-headers"] \ No newline at end of file + "--proxy-headers"] diff --git a/app/api/author.py b/app/api/author.py index e54bb3e..4d61798 100644 --- a/app/api/author.py +++ b/app/api/author.py @@ -1,43 +1,41 @@ -from fastapi import APIRouter, HTTPException, Query, Path from typing import Annotated from uuid import UUID +from fastapi import APIRouter, HTTPException, Path, Query + from app.crud.author import Author from app.schemas.author import AuthorListModel, AuthorOutputModel -from app.schemas.query import FilterWorkstream, FilterParams +from app.schemas.query import FilterParams, FilterWorkstream router = APIRouter(prefix="/api/authors", tags=["authors"]) @router.get("") -def api_author_list(query: Annotated[FilterWorkstream, Query()] - ) -> AuthorListModel: +def api_author_list(query: Annotated[FilterWorkstream, Query()]) -> AuthorListModel: try: authors = Author() - if result := authors.get_authors(skip=query.skip, - limit=query.limit, - workstream=query.workstream): + if result := authors.get_authors( + skip=query.skip, limit=query.limit, workstream=query.workstream + ): return result except Exception as e: raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") from e + @router.get("/{id}") -def api_author(id: Annotated[UUID, Path(title="Unique author identifier")], - query: Annotated[FilterParams, Query()] - ) -> AuthorOutputModel: +def api_author( + id: Annotated[UUID, Path(title="Unique author identifier")], + query: Annotated[FilterParams, Query()], +) -> AuthorOutputModel: author = Author() try: - result = author.get_author(id=id, - result_type=query.result_type, - skip=query.skip, - limit=query.limit) + result = author.get_author( + id=id, result_type=query.result_type, skip=query.skip, limit=query.limit + ) except KeyError: - raise HTTPException(status_code=404, - detail=f"Author '{id}' not found") + raise HTTPException(status_code=404, detail=f"Author '{id}' not found") except ValueError as e: - raise HTTPException(status_code=500, - detail=f"Database error: {str(e)}") from e + raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") from e else: return result - diff --git a/app/api/country.py b/app/api/country.py index 1390d5e..3dbb84c 100644 --- a/app/api/country.py +++ b/app/api/country.py @@ -1,5 +1,7 @@ -from fastapi import APIRouter, HTTPException, Query, Path from typing import Annotated + +from fastapi import APIRouter, HTTPException, Path, Query + from app.crud.country import Country from app.schemas.country import CountryList, CountryOutputListModel from app.schemas.query import FilterBase, FilterParams @@ -8,31 +10,30 @@ @router.get("") -def api_country_list(query: Annotated[FilterBase, Query()] - ) -> CountryList: +def api_country_list(query: Annotated[FilterBase, Query()]) -> CountryList: country_model = Country() try: return country_model.get_countries(query.skip, query.limit) except Exception as e: - raise HTTPException(status_code=500, - detail=f"Server error: {str(e)}") from e + raise HTTPException(status_code=500, detail=f"Server error: {str(e)}") from e + @router.get("/{id}") -def api_country(id: Annotated[str, Path(examples=['KEN'], title="Country identifier", pattern="^([A-Z]{3})$")], - query: Annotated[FilterParams, Query()] - ) -> CountryOutputListModel: +def api_country( + id: Annotated[ + str, Path(examples=["KEN"], title="Country identifier", pattern="^([A-Z]{3})$") + ], + query: Annotated[FilterParams, Query()], +) -> CountryOutputListModel: country_model = Country() try: - result = country_model.get_country(id, - query.skip, - query.limit, - query.result_type) + result = country_model.get_country( + id, query.skip, query.limit, query.result_type + ) except KeyError: - raise HTTPException(status_code=404, - detail=f"Country with id {id} not found") + raise HTTPException(status_code=404, detail=f"Country with id {id} not found") except ValueError as e: - raise HTTPException(status_code=500, - detail=f"Database error: {str(e)}") from e + raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") from e else: return result diff --git a/app/api/output.py b/app/api/output.py index 447961e..4d3a9bc 100644 --- a/app/api/output.py +++ b/app/api/output.py @@ -1,39 +1,39 @@ -from fastapi import APIRouter, HTTPException, Query, Path -from fastapi.logger import logger from typing import Annotated -from app.schemas.query import FilterOutputList from uuid import UUID +from fastapi import APIRouter, HTTPException, Path, Query +from fastapi.logger import logger + from app.crud.output import Output from app.schemas.output import OutputListModel, OutputModel +from app.schemas.query import FilterOutputList router = APIRouter(prefix="/api/outputs", tags=["outputs"]) @router.get("") -def api_output_list( - query: Annotated[FilterOutputList, Query()] -) -> OutputListModel: +def api_output_list(query: Annotated[FilterOutputList, Query()]) -> OutputListModel: """Return a list of outputs""" outputs = Output() try: - return outputs.get_outputs(query.skip, - query.limit, - query.result_type, - query.country) + return outputs.get_outputs( + query.skip, query.limit, query.result_type, query.country + ) except ValueError as e: raise HTTPException(status_code=500, detail=str(e)) from e @router.get("/{id}") -def api_output(id: Annotated[UUID, Path(title="Unique output identifier")]) -> OutputModel: +def api_output( + id: Annotated[UUID, Path(title="Unique output identifier")], +) -> OutputModel: output = Output() try: result = output.get_output(id) except KeyError as e: raise HTTPException( - status_code=404, detail=f"Output with id {id} not found" - ) from e + status_code=404, detail=f"Output with id {id} not found" + ) from e except Exception as e: logger.error(f"Error in api_output: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) from e diff --git a/app/api/workstream.py b/app/api/workstream.py index 4db8b09..53d54f3 100644 --- a/app/api/workstream.py +++ b/app/api/workstream.py @@ -1,17 +1,16 @@ -from typing import List, Annotated +from typing import Annotated, List -from fastapi import APIRouter, HTTPException, Query, Path +from fastapi import APIRouter, HTTPException, Path, Query from app.crud.workstream import Workstream -from app.schemas.workstream import WorkstreamDetailModel, WorkstreamListModel from app.schemas.query import FilterBase +from app.schemas.workstream import WorkstreamDetailModel, WorkstreamListModel router = APIRouter(prefix="/api/workstreams", tags=["workstreams"]) @router.get("") -def list_workstreams( - query: Annotated[FilterBase, Query()]) -> WorkstreamListModel: +def list_workstreams(query: Annotated[FilterBase, Query()]) -> WorkstreamListModel: """Return a list of workstreams Returns @@ -23,25 +22,21 @@ def list_workstreams( try: results = model.get_all(skip=query.skip, limit=query.limit) except KeyError as e: - raise HTTPException(status_code=500, - detail=f"Database error: {str(e)}") from e + raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") from e else: return results - @router.get("/{id}") def get_workstream( id: Annotated[str, Path(title="Unique workstream identifier")], - query: Annotated[FilterBase, Query()] - ) -> WorkstreamDetailModel: - """Return a single workstream - """ + query: Annotated[FilterBase, Query()], +) -> WorkstreamDetailModel: + """Return a single workstream""" model = Workstream() try: results = model.get(id, skip=query.skip, limit=query.limit) except KeyError: - raise HTTPException(status_code=404, - detail=f"Workstream '{id}' not found") + raise HTTPException(status_code=404, detail=f"Workstream '{id}' not found") else: - return results \ No newline at end of file + return results diff --git a/app/core/config.py b/app/core/config.py index dc695a4..70d9138 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,4 +1,5 @@ import os + from dotenv import load_dotenv diff --git a/app/crud/author.py b/app/crud/author.py index d309e17..99dd41b 100644 --- a/app/crud/author.py +++ b/app/crud/author.py @@ -2,22 +2,18 @@ from uuid import UUID from fastapi.logger import logger - from neo4j import Driver from app.db.session import connect_to_db -from app.schemas.author import (AuthorListModel, - AuthorOutputModel, - AuthorColabModel) +from app.schemas.author import AuthorColabModel, AuthorListModel, AuthorOutputModel from app.schemas.meta import CountPublication class Author: - def get_authors(self, - skip: int, - limit: int, - workstream: list[str] = []) -> AuthorListModel: + def get_authors( + self, skip: int, limit: int, workstream: list[str] = [] + ) -> AuthorListModel: """Get list of authors Arguments @@ -37,13 +33,14 @@ def get_authors(self, count = len(authors) else: count = self.count_authors() - return {"meta": { - "count": {"total": count}, - "skip": skip, - "limit": limit}, - "results": authors} - - def get_author(self, id: UUID, result_type: str = 'publication', skip: int = 0, limit: int = 20) -> AuthorOutputModel: + return { + "meta": {"count": {"total": count}, "skip": skip, "limit": limit}, + "results": authors, + } + + def get_author( + self, id: UUID, result_type: str = "publication", skip: int = 0, limit: int = 20 + ) -> AuthorOutputModel: """Get an author, collaborators and outputs Arguments @@ -62,16 +59,17 @@ def get_author(self, id: UUID, result_type: str = 'publication', skip: int = 0, collaborators = self.fetch_collaborator_nodes(str(id), result_type)[0] collaborators = [collaborator.data() for collaborator in collaborators] count = self.count_author_outputs(str(id)) - publications = self.fetch_publications(str(id), - result_type=result_type, - skip=skip, - limit=limit) - author['collaborators'] = collaborators - author['outputs'] = {'results': publications} - author['outputs']['meta'] = {"count": count, - "skip": skip, - "limit": limit, - "result_type": result_type} + publications = self.fetch_publications( + str(id), result_type=result_type, skip=skip, limit=limit + ) + author["collaborators"] = collaborators + author["outputs"] = {"results": publications} + author["outputs"]["meta"] = { + "count": count, + "skip": skip, + "limit": limit, + "result_type": result_type, + } return author else: msg = f"Could not find author with id: {id}" @@ -108,7 +106,9 @@ def fetch_author_nodes( ORDER BY last_name SKIP $skip LIMIT $limit;""" - records, _, _ = db.execute_query(query, skip=skip, limit=limit, workstream=workstream) + records, _, _ = db.execute_query( + query, skip=skip, limit=limit, workstream=workstream + ) return [record.data() for record in records] @connect_to_db @@ -121,8 +121,6 @@ def count_authors(self, db: Driver) -> int: return [record.data() for record in records][0]["count"] - - @connect_to_db def fetch_author_node(self, id: str, db: Driver) -> Dict[str, Any]: author_query = """ @@ -249,5 +247,3 @@ def fetch_publications( publications.append(package) return publications - - diff --git a/app/crud/country.py b/app/crud/country.py index e57b3bd..c5f2687 100644 --- a/app/crud/country.py +++ b/app/crud/country.py @@ -1,25 +1,20 @@ from typing import Any, Dict -from fastapi.logger import logger +from fastapi.logger import logger from neo4j import Driver from app.db.session import connect_to_db -from .output import Output - -from app.schemas.country import (CountryList, - CountryNodeModel, - CountryOutputListModel) +from app.schemas.country import CountryList, CountryNodeModel, CountryOutputListModel from app.schemas.meta import CountPublication +from .output import Output + class Country: - def get_country(self, - id: str, - skip: int = 0, - limit: int = 20, - result_type: str = 'publication' - ) -> CountryOutputListModel: + def get_country( + self, id: str, skip: int = 0, limit: int = 20, result_type: str = "publication" + ) -> CountryOutputListModel: """Return a country Arguments @@ -37,16 +32,14 @@ def get_country(self, try: entity = self.fetch_country_node(id) except KeyError as ex: - logger.error( - f"Country outputs not found {id}:{skip}:{limit}:{result_type}") + logger.error(f"Country outputs not found {id}:{skip}:{limit}:{result_type}") ex.add_note(f"Could not find {id} in the db") raise KeyError(ex) else: outputs = Output() - package = outputs.get_outputs(skip=skip, - limit=limit, - result_type=result_type, - country=id) + package = outputs.get_outputs( + skip=skip, limit=limit, result_type=result_type, country=id + ) counts = self.count_country_outputs(id) package["meta"]["count"] = counts return package | entity @@ -65,8 +58,10 @@ def get_countries(self, skip: int = 0, limit: int = 20) -> CountryList: """ results = self.get_country_list(skip=skip, limit=limit) count = self.count_countries() - return {"meta": {"count": {'total': count}, "skip": skip, "limit": limit}, - "results": results} + return { + "meta": {"count": {"total": count}, "skip": skip, "limit": limit}, + "results": results, + } @connect_to_db def fetch_country_node(self, id: str, db: Driver) -> Dict[str, Any]: @@ -120,13 +115,15 @@ def count_country_outputs(self, id: str, db: Driver) -> CountPublication: """ records, _, _ = db.execute_query(query, id=id) if len(records) <= 0: - return {'total': 0, - 'publication': 0, - 'dataset': 0, - 'other': 0, - 'software': 0} + return { + "total": 0, + "publication": 0, + "dataset": 0, + "other": 0, + "software": 0, + } counts = {x.data()["result_type"]: x.data()["count"] for x in records} - counts['total'] = sum(counts.values()) + counts["total"] = sum(counts.values()) return CountPublication(**counts) @@ -136,13 +133,12 @@ def count_countries(self, db: Driver) -> int: query = """MATCH (c:Country)<-[:refers_to]-(p:Output) RETURN count(DISTINCT c.id) as count""" results, _, _ = db.execute_query(query) - return results[0].data()['count'] + return results[0].data()["count"] @connect_to_db - def get_country_list(self, - db: Driver, - skip: int = 0, - limit: int = 20) -> list[CountryNodeModel]: + def get_country_list( + self, db: Driver, skip: int = 0, limit: int = 20 + ) -> list[CountryNodeModel]: """Retrieve all countries that have associated articles. Parameters @@ -160,4 +156,4 @@ def get_country_list(self, LIMIT $limit """ records, _, _ = db.execute_query(query, skip=skip, limit=limit) - return [result.data()['country'] for result in records] + return [result.data()["country"] for result in records] diff --git a/app/crud/ingest.py b/app/crud/ingest.py index 2dfa1f8..82b2890 100644 --- a/app/crud/ingest.py +++ b/app/crud/ingest.py @@ -1,8 +1,9 @@ from typing import Tuple -from app.schemas.ingest import IngestionMetrics, IngestionStates from research_index_backend.create_graph_from_doi import add_country_relations, main +from app.schemas.ingest import IngestionMetrics, IngestionStates + class Ingest: def __init__( diff --git a/app/crud/output.py b/app/crud/output.py index 777cae2..bdb8f96 100644 --- a/app/crud/output.py +++ b/app/crud/output.py @@ -1,7 +1,8 @@ from typing import Any, Dict, List from uuid import UUID -from neo4j import Driver + from fastapi.logger import logger +from neo4j import Driver from app.db.session import connect_to_db from app.schemas.output import OutputListModel, OutputModel @@ -54,9 +55,9 @@ def get_output(self, id: UUID, db: Driver) -> OutputModel: records, _, _ = db.execute_query(query, uuid=str(id)) if records: data = [x.data() for x in records][0] - package = data['outputs'] - package['authors'] = data['authors'] - package['countries'] = data['countries'] + package = data["outputs"] + package["authors"] = data["authors"] + package["countries"] = data["countries"] return package else: @@ -84,17 +85,21 @@ def count(self, db: Driver) -> Dict[str, int]: """ records, _, _ = db.execute_query(query) if len(records) <= 0: - return {'total': 0, - 'publication': 0, - 'dataset': 0, - 'other': 0, - 'software': 0} + return { + "total": 0, + "publication": 0, + "dataset": 0, + "other": 0, + "software": 0, + } counts = {x.data()["result_type"]: x.data()["count"] for x in records} - counts['total'] = sum(counts.values()) + counts["total"] = sum(counts.values()) return counts @connect_to_db - def filter_type(self, db: Driver, result_type: str, skip: int, limit: int) -> List[Dict[str, Any]]: + def filter_type( + self, db: Driver, result_type: str, skip: int, limit: int + ) -> List[Dict[str, Any]]: """Filter articles by result type and return with ordered authors. Parameters @@ -138,27 +143,23 @@ def filter_type(self, db: Driver, result_type: str, skip: int, limit: int) -> Li SKIP $skip LIMIT $limit; """ - records, _, _ = db.execute_query(query, - result_type=result_type, - skip=skip, - limit=limit) + records, _, _ = db.execute_query( + query, result_type=result_type, skip=skip, limit=limit + ) outputs = [] for x in records: data = x.data() - package = data['outputs'] - package['authors'] = data['authors'] - package['countries'] = data['countries'] + package = data["outputs"] + package["authors"] = data["authors"] + package["countries"] = data["countries"] outputs.append(package) return outputs @connect_to_db - def filter_country(self, - db: Driver, - result_type: str, - skip: int, - limit: int, - country: str) -> List[Dict[str, Any]]: + def filter_country( + self, db: Driver, result_type: str, skip: int, limit: int, country: str + ) -> List[Dict[str, Any]]: """Filter articles by country and result type and return with ordered authors. Parameters @@ -207,26 +208,26 @@ def filter_country(self, SKIP $skip LIMIT $limit; """ - records, summary, keys = db.execute_query(query, - result_type=result_type, - country_id=country, - skip=skip, - limit=limit) + records, summary, keys = db.execute_query( + query, result_type=result_type, country_id=country, skip=skip, limit=limit + ) outputs = [] for x in records: data = x.data() - package = data['outputs'] - package['authors'] = data['authors'] - package['countries'] = data['countries'] + package = data["outputs"] + package["authors"] = data["authors"] + package["countries"] = data["countries"] outputs.append(package) return outputs - def get_outputs(self, - skip: int = 0, - limit: int = 20, - result_type: str = 'publication', - country: str = None) -> OutputListModel: + def get_outputs( + self, + skip: int = 0, + limit: int = 20, + result_type: str = "publication", + country: str = None, + ) -> OutputListModel: """Return a list of outputs""" try: if country: @@ -234,9 +235,9 @@ def get_outputs(self, result_type=result_type, skip=skip, limit=limit, country=country ) else: - results = self.filter_type(result_type=result_type, - skip=skip, - limit=limit) + results = self.filter_type( + result_type=result_type, skip=skip, limit=limit + ) count = self.count() @@ -245,9 +246,9 @@ def get_outputs(self, "count": count, "skip": skip, "limit": limit, - "result_type": result_type + "result_type": result_type, }, "results": results, } except ValueError as e: - raise ValueError(str(e)) from e \ No newline at end of file + raise ValueError(str(e)) from e diff --git a/app/crud/workstream.py b/app/crud/workstream.py index 1ebde77..ab81b8d 100644 --- a/app/crud/workstream.py +++ b/app/crud/workstream.py @@ -3,12 +3,18 @@ from neo4j import Driver from app.db.session import connect_to_db -from app.schemas.workstream import WorkstreamDetailModel, WorkstreamListModel, WorkstreamBase -from .author import Author from app.schemas.author import AuthorListModel from app.schemas.output import OutputListModel +from app.schemas.workstream import ( + WorkstreamBase, + WorkstreamDetailModel, + WorkstreamListModel, +) + +from .author import Author from .output import Output + class Workstream: def get_all(self, skip: int = 0, limit: int = 20) -> WorkstreamListModel: @@ -25,15 +31,16 @@ def get_all(self, skip: int = 0, limit: int = 20) -> WorkstreamListModel: ------- app.schema.workstream.WorkstreamListModel """ - return {'results': self.get_workstreams(skip=skip, limit=limit), - 'meta': {'count': {'total': self.count_members()}, - 'skip': skip, - 'limit': limit}} - - def get(self, - id: str, - skip: int = 0, - limit: int = 20) -> WorkstreamDetailModel: + return { + "results": self.get_workstreams(skip=skip, limit=limit), + "meta": { + "count": {"total": self.count_members()}, + "skip": skip, + "limit": limit, + }, + } + + def get(self, id: str, skip: int = 0, limit: int = 20) -> WorkstreamDetailModel: """Return a list of members for a workstream Arguments @@ -52,12 +59,12 @@ def get(self, """ workstream = self.get_workstream_detail(id) if workstream: - members = self.get_members([id] + workstream.pop('children'), - skip=skip, - limit=limit) # typing: AuthorListModel - return workstream | {'members': members} + members = self.get_members( + [id] + workstream.pop("children"), skip=skip, limit=limit + ) # typing: AuthorListModel + return workstream | {"members": members} else: - return {'members': {}} + return {"members": {}} @connect_to_db def get_workstream_detail(self, id: str, db: Driver) -> dict: @@ -77,11 +84,9 @@ def get_outputs(self, id: str, skip, limit) -> OutputListModel: return output.get_outputs(skip, limit) @connect_to_db - def get_workstreams(self, - db: Driver, - skip: int = 0, - limit: int = 20 - ) -> list[WorkstreamBase]: + def get_workstreams( + self, db: Driver, skip: int = 0, limit: int = 20 + ) -> list[WorkstreamBase]: query = """MATCH (p:Workstream)-[]-(:Author) OPTIONAL MATCH (u:Workstream)<-[:unit_of]-(p) RETURN DISTINCT u.id as unit_id, u.name as unit_name, p.id as id, p.name as name @@ -94,10 +99,9 @@ def get_workstreams(self, else: return [x.data() for x in records] - def get_members(self, - id: list[str], - skip: int = 0, - limit: int = 20) -> AuthorListModel: + def get_members( + self, id: list[str], skip: int = 0, limit: int = 20 + ) -> AuthorListModel: author = Author() return author.get_authors(skip, limit, id) @@ -106,4 +110,4 @@ def count_members(self, db: Driver) -> int: query = """MATCH (p:Workstream) RETURN count(DISTINCT p) as count""" records, _, _ = db.execute_query(query) - return records[0].data()['count'] + return records[0].data()["count"] diff --git a/app/db/session.py b/app/db/session.py index ead13f2..0ab2f51 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -1,9 +1,9 @@ from functools import wraps -from app.core.config import settings - from neo4j import GraphDatabase +from app.core.config import settings + MG_HOST = settings.MG_HOST MG_PORT = settings.MG_PORT MG_USER = settings.MG_USER diff --git a/app/main.py b/app/main.py index 3ea5af0..439d4dd 100644 --- a/app/main.py +++ b/app/main.py @@ -1,31 +1,32 @@ +import logging +from typing import Annotated +from uuid import UUID + +from fastapi import FastAPI, HTTPException, Path, Query, Request from fastapi.logger import logger -from fastapi import FastAPI, Request, Query, Path, HTTPException +from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -from fastapi.middleware.cors import CORSMiddleware - -from typing import Annotated -from uuid import UUID +from app.api import author, country, ingest, output, workstream from app.crud.author import Author from app.crud.country import Country from app.crud.output import Output from app.crud.workstream import Workstream -from app.schemas.query import (FilterWorkstream, FilterParams, FilterBase, FilterOutputList) - -from app.api import author, country, ingest, output, workstream - -import logging +from app.schemas.query import ( + FilterBase, + FilterOutputList, + FilterParams, + FilterWorkstream, +) # Add console handler to the fastapi logger console_handler = logging.StreamHandler() logger.addHandler(console_handler) # Use a nice format for the log messages -formatter = logging.Formatter( - "%(asctime)s [%(levelname)s] %(message)s" -) +formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") console_handler.setFormatter(formatter) # Obtain access loggers for uvicorn @@ -39,8 +40,8 @@ allow_origins=["*"], allow_credentials=False, allow_methods=["GET", "POST", "PUT", "DELETE"], - allow_headers=["*"] - ) + allow_headers=["*"], +) app.include_router(author.router) app.include_router(country.router) @@ -57,134 +58,117 @@ def index(request: Request): countries = Country().get_countries(skip=0, limit=200) return templates.TemplateResponse( - request, - "index.html", - {"title": "Home"} | countries + request, "index.html", {"title": "Home"} | countries ) @app.get("/countries/{id}", response_class=HTMLResponse) -def country(request: Request, - id: Annotated[str, Path(examples=['KEN'], title="Country identifier", pattern="^([A-Z]{3})$")], - query: Annotated[FilterParams, Query()] - ): +def country( + request: Request, + id: Annotated[ + str, Path(examples=["KEN"], title="Country identifier", pattern="^([A-Z]{3})$") + ], + query: Annotated[FilterParams, Query()], +): country_model = Country() try: - country = country_model.get_country(id, query.skip, query.limit, query.result_type) + country = country_model.get_country( + id, query.skip, query.limit, query.result_type + ) except KeyError: - raise HTTPException(status_code=404, - detail=f"Country with id '{id}' not found") + raise HTTPException(status_code=404, detail=f"Country with id '{id}' not found") except Exception as e: - raise HTTPException(status_code=500, - detail=f"Database error: {str(e)}") from e + raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") from e else: return templates.TemplateResponse( - request, - "country.html", - {"title": "Country"} | country + request, "country.html", {"title": "Country"} | country ) @app.get("/countries", response_class=HTMLResponse) -def country_list(request: Request, - query: Annotated[FilterBase, Query()] - - ): +def country_list(request: Request, query: Annotated[FilterBase, Query()]): country_model = Country() try: entity = country_model.get_countries(query.skip, query.limit) except Exception as e: - raise HTTPException(status_code=500, - detail=f"Server error: {str(e)}") from e + raise HTTPException(status_code=500, detail=f"Server error: {str(e)}") from e else: return templates.TemplateResponse( - request, - "country_list.html", - {"title": "Countries"} | entity + request, "country_list.html", {"title": "Countries"} | entity ) @app.get("/authors/{id}", response_class=HTMLResponse) -def author(request: Request, - id: Annotated[UUID, Path(title="Unique author identifier")], - query: Annotated[FilterParams, Query()]): +def author( + request: Request, + id: Annotated[UUID, Path(title="Unique author identifier")], + query: Annotated[FilterParams, Query()], +): author = Author() try: entity = author.get_author( - id, - result_type=query.result_type, - skip=query.skip, - limit=query.limit) + id, result_type=query.result_type, skip=query.skip, limit=query.limit + ) except KeyError: - raise HTTPException(status_code=404, - detail=f"Author '{id}' not found") + raise HTTPException(status_code=404, detail=f"Author '{id}' not found") else: return templates.TemplateResponse( - request, - "author.html", - {"title": "Author"} | entity # Merges dicts + request, "author.html", {"title": "Author"} | entity # Merges dicts ) @app.get("/authors", response_class=HTMLResponse) -def author_list(request: Request, - query: Annotated[FilterWorkstream, Query()]): +def author_list(request: Request, query: Annotated[FilterWorkstream, Query()]): authors = Author() try: - entity = authors.get_authors(skip=query.skip, - limit=query.limit, - workstream=query.workstream) + entity = authors.get_authors( + skip=query.skip, limit=query.limit, workstream=query.workstream + ) except KeyError as ex: - raise HTTPException(status_code=404, - detail=f"Authors not found") + raise HTTPException(status_code=404, detail=f"Authors not found") else: return templates.TemplateResponse( - request, - "authors.html", - {"title": "Author List"} | entity # Merges dicts + request, "authors.html", {"title": "Author List"} | entity # Merges dicts ) @app.get("/outputs", response_class=HTMLResponse) -def output_list(request: Request, - query: Annotated[FilterOutputList, Query()] - ): +def output_list(request: Request, query: Annotated[FilterOutputList, Query()]): model = Output() try: - package = model.get_outputs(skip=query.skip, - limit=query.limit, - result_type=query.result_type, - country=query.country) + package = model.get_outputs( + skip=query.skip, + limit=query.limit, + result_type=query.result_type, + country=query.country, + ) except ValueError as e: raise HTTPException(status_code=500, detail=str(e)) from e else: return templates.TemplateResponse( - request, - "outputs.html", - {"title": "Output List"} | package + request, "outputs.html", {"title": "Output List"} | package ) @app.get("/outputs/{id}", response_class=HTMLResponse) -def output(request: Request, - id: Annotated[UUID, Path(title="Unique output identifier")] - ): +def output( + request: Request, id: Annotated[UUID, Path(title="Unique output identifier")] +): output_model = Output() try: entity = output_model.get_output(id) except KeyError as e: raise HTTPException( - status_code=404, detail=f"Output with id {id} not found" - ) from e + status_code=404, detail=f"Output with id {id} not found" + ) from e except Exception as e: logger.error(f"Error in api_output: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) from e else: return templates.TemplateResponse( - request, - "output.html", - {"title": "Output"} | entity) + request, "output.html", {"title": "Output"} | entity + ) @app.get("/workstreams", response_class=HTMLResponse) @@ -193,41 +177,33 @@ def workstream_list(request: Request): try: all = model.get_all() except KeyError as e: - raise HTTPException(status_code=500, - detail=f"Database error: {str(e)}") from e + raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") from e else: try: - entity = model.get(all['results'][0]['id']) + entity = model.get(all["results"][0]["id"]) except KeyError as e: - raise HTTPException(status_code=500, - detail=f"Database error: {str(e)}") from e + raise HTTPException( + status_code=500, detail=f"Database error: {str(e)}" + ) from e else: return templates.TemplateResponse( - request, - "workstreams.html", - {"title": "Workstream"} | entity | all - ) + request, "workstreams.html", {"title": "Workstream"} | entity | all + ) @app.get("/workstreams/{id}", response_class=HTMLResponse) -def workstream(request: Request, - id: str, - query: Annotated[FilterBase, Query()] - ): +def workstream(request: Request, id: str, query: Annotated[FilterBase, Query()]): model = Workstream() all = model.get_all() try: entity = model.get(id, skip=query.skip, limit=query.limit) except KeyError as e: - raise HTTPException(status_code=404, - detail=f"Workstream '{id}' not found") + raise HTTPException(status_code=404, detail=f"Workstream '{id}' not found") else: return templates.TemplateResponse( - request, - "workstreams.html", - {"title": "Workstreams"} | entity | all + request, "workstreams.html", {"title": "Workstreams"} | entity | all ) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index c63aef4..be50412 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -1,7 +1,7 @@ from typing import Dict, List, Optional +from uuid import UUID from pydantic import BaseModel, Field, HttpUrl -from uuid import UUID class AuthorBase(BaseModel): @@ -10,12 +10,10 @@ class AuthorBase(BaseModel): with their associated metadata. """ - uuid: UUID = Field(..., - description="Unique identifier for the author") + uuid: UUID = Field(..., description="Unique identifier for the author") first_name: str = Field(..., min_length=1) last_name: str = Field(..., min_length=1) - orcid: Optional[HttpUrl] = \ - Field(None, description="Author's ORCID identifier") + orcid: Optional[HttpUrl] = Field(None, description="Author's ORCID identifier") class CountryBaseModel(BaseModel): @@ -25,4 +23,4 @@ class CountryBaseModel(BaseModel): class WorkstreamBase(BaseModel): id: Optional[str] = None - name: Optional[str] = None \ No newline at end of file + name: Optional[str] = None diff --git a/app/schemas/affiliation.py b/app/schemas/affiliation.py index fdfbe4a..90d164b 100644 --- a/app/schemas/affiliation.py +++ b/app/schemas/affiliation.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + class AffiliationModel(BaseModel): id: Optional[str] = None diff --git a/app/schemas/author.py b/app/schemas/author.py index 25b1fdf..9a5bc1d 100644 --- a/app/schemas/author.py +++ b/app/schemas/author.py @@ -1,13 +1,12 @@ from typing import Dict, List, Optional +from uuid import UUID from pydantic import BaseModel, Field, HttpUrl -from uuid import UUID -from . import AuthorBase +from . import AuthorBase, WorkstreamBase from .affiliation import AffiliationModel -from .output import OutputListModel from .meta import MetaAuthor -from . import WorkstreamBase +from .output import OutputListModel class AuthorColabModel(AuthorBase): @@ -15,6 +14,7 @@ class AuthorColabModel(AuthorBase): An academic author or contributor with their collaborators, workstreams and affiliations """ + workstreams: List[WorkstreamBase] = None affiliations: List[AffiliationModel] = None @@ -23,11 +23,13 @@ class AuthorListModel(BaseModel): """ A list of authors """ + meta: MetaAuthor results: List[AuthorColabModel] class AuthorOutputModel(AuthorColabModel): """An author with collaborators, workstreams, affiliations and outputs""" + collaborators: List[AuthorBase] = None outputs: OutputListModel diff --git a/app/schemas/country.py b/app/schemas/country.py index 8e31000..2eb84a4 100644 --- a/app/schemas/country.py +++ b/app/schemas/country.py @@ -1,15 +1,16 @@ from typing import Dict, List, Optional + from pydantic import BaseModel, Field -from . meta import Pagination -from . output import OutputListModel + from . import CountryBaseModel +from .meta import Pagination +from .output import OutputListModel class CountryOutputListModel(OutputListModel, CountryBaseModel): """Data model representing country outputs""" - class CountryNodeModel(CountryBaseModel): dbpedia: Optional[str] = None latitude: Optional[float] = None diff --git a/app/schemas/meta.py b/app/schemas/meta.py index 0a15ab1..2c7567b 100644 --- a/app/schemas/meta.py +++ b/app/schemas/meta.py @@ -1,4 +1,5 @@ from typing import Dict, List, Optional + from pydantic import BaseModel, Field @@ -22,6 +23,7 @@ class CountPublication(BaseModel): class CountAuthor(BaseModel): """Represents a count of the authors or countries""" + total: int = 0 @@ -47,6 +49,7 @@ class MetaPublication(BaseModel): ``` """ + count: CountPublication skip: int limit: int @@ -63,6 +66,7 @@ class MetaAuthor(BaseModel): ``` """ + count: CountAuthor | None skip: int limit: int diff --git a/app/schemas/output.py b/app/schemas/output.py index b617a52..7bb75ef 100644 --- a/app/schemas/output.py +++ b/app/schemas/output.py @@ -1,8 +1,9 @@ -from pydantic import BaseModel, Field, HttpUrl from typing import List, Optional from uuid import UUID -from . import AuthorBase -from . import CountryBaseModel + +from pydantic import BaseModel, Field, HttpUrl + +from . import AuthorBase, CountryBaseModel from .meta import MetaPublication from .topic import TopicBaseModel @@ -60,6 +61,7 @@ class OutputModel(BaseModel): } ``` """ + uuid: UUID doi: str = Field(pattern=r"^10\.\d{4,9}/[-._;()/:a-zA-Z0-9]+$") title: str @@ -78,8 +80,7 @@ class OutputModel(BaseModel): class OutputListModel(BaseModel): - """Represents a list of outputs including metadata + """Represents a list of outputs including metadata""" - """ meta: MetaPublication results: List[OutputModel] diff --git a/app/schemas/query.py b/app/schemas/query.py index 5745a4c..a8b53b2 100644 --- a/app/schemas/query.py +++ b/app/schemas/query.py @@ -1,12 +1,16 @@ -from typing import Literal, List +from typing import List, Literal -from pydantic import BaseModel, Field, field_validator from fastapi import HTTPException +from pydantic import BaseModel, Field, field_validator class FilterBase(BaseModel): - skip: int = Field(default=0, ge=0, title="Skip", description="Number of records to skip") - limit: int = Field(default=20, ge=1, title="Limit", description="Number of records to return") + skip: int = Field( + default=0, ge=0, title="Skip", description="Number of records to skip" + ) + limit: int = Field( + default=20, ge=1, title="Limit", description="Number of records to return" + ) class FilterParams(FilterBase): @@ -14,15 +18,16 @@ class FilterParams(FilterBase): class FilterCountry(FilterParams): - country: str | None = Field(default=None, examples=['KEN'], pattern="^([A-Z]{3})$") + country: str | None = Field(default=None, examples=["KEN"], pattern="^([A-Z]{3})$") class FilterOutputList(FilterCountry): pass + class FilterWorkstream(FilterBase): workstream: List[str] | None = Field(default=None) class FilterAuthorDetail(FilterCountry): - pass \ No newline at end of file + pass diff --git a/app/schemas/topic.py b/app/schemas/topic.py index 4e24a3c..e46b59c 100644 --- a/app/schemas/topic.py +++ b/app/schemas/topic.py @@ -3,10 +3,12 @@ See the OpenAlex documentation: https://docs.openalex.org/api-entities/topics/topic-object """ + from typing import Dict, List -from pydantic import BaseModel, HttpUrl from uuid import UUID +from pydantic import BaseModel, HttpUrl + class DomainModel(BaseModel): id: int @@ -25,6 +27,7 @@ class TopicBaseModel(BaseModel): https://docs.openalex.org/api-entities/topics/topic-object """ + id: UUID openalex_id: HttpUrl description: str diff --git a/app/schemas/workstream.py b/app/schemas/workstream.py index 49c5ad2..82c4edc 100644 --- a/app/schemas/workstream.py +++ b/app/schemas/workstream.py @@ -1,8 +1,9 @@ from pydantic import BaseModel -from .output import OutputListModel -from .meta import MetaAuthor -from .author import AuthorListModel + from . import WorkstreamBase +from .author import AuthorListModel +from .meta import MetaAuthor +from .output import OutputListModel class WorkstreamDetailModel(WorkstreamBase): diff --git a/app/static/js/map.js b/app/static/js/map.js index 794b379..5c07669 100644 --- a/app/static/js/map.js +++ b/app/static/js/map.js @@ -79,4 +79,4 @@ function draw_map(country) { .style("stroke", "none") }) -} \ No newline at end of file +} diff --git a/app/static/js/network.js b/app/static/js/network.js index 8c64c0b..b9619fd 100644 --- a/app/static/js/network.js +++ b/app/static/js/network.js @@ -126,4 +126,4 @@ function dragended(event) { // stop naturally, but it’s a good practice.) // invalidation.then(() => simulation.stop()); } -main(); \ No newline at end of file +main(); diff --git a/app/static/js/world.js b/app/static/js/world.js index 7538d2e..1d2adf3 100644 --- a/app/static/js/world.js +++ b/app/static/js/world.js @@ -94,4 +94,3 @@ d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/w .attr("alignment-baseline", "middle"); }); }) - diff --git a/app/templates/author.html b/app/templates/author.html index c0b4959..e57d826 100644 --- a/app/templates/author.html +++ b/app/templates/author.html @@ -75,4 +75,4 @@
Top Collaborators for {{ outputs.meta.result_type|title } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/author_list.html b/app/templates/author_list.html index fedfcee..47cf384 100644 --- a/app/templates/author_list.html +++ b/app/templates/author_list.html @@ -18,4 +18,4 @@ {% endif %} -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/app/templates/authors.html b/app/templates/authors.html index 1ac396e..7e19967 100644 --- a/app/templates/authors.html +++ b/app/templates/authors.html @@ -6,4 +6,4 @@ {% include 'pagination.html' %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index ed34a22..157e0b7 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -53,4 +53,4 @@ - \ No newline at end of file + diff --git a/app/templates/country.html b/app/templates/country.html index 7f65de7..ea2d1b0 100644 --- a/app/templates/country.html +++ b/app/templates/country.html @@ -10,4 +10,4 @@

{{ name }}

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/country_list.html b/app/templates/country_list.html index a1ed7f8..d1e9a47 100644 --- a/app/templates/country_list.html +++ b/app/templates/country_list.html @@ -32,4 +32,4 @@
{{country.name}}
{% include 'pagination.html' %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/header.html b/app/templates/header.html index 982949e..973d64e 100644 --- a/app/templates/header.html +++ b/app/templates/header.html @@ -53,4 +53,4 @@ - \ No newline at end of file + diff --git a/app/templates/output.html b/app/templates/output.html index b279a5d..ccca2b8 100644 --- a/app/templates/output.html +++ b/app/templates/output.html @@ -83,4 +83,4 @@
Abstract
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/output_list.j2 b/app/templates/output_list.j2 index 2c6d278..94e9691 100644 --- a/app/templates/output_list.j2 +++ b/app/templates/output_list.j2 @@ -79,4 +79,4 @@ {% endif %} - \ No newline at end of file + diff --git a/app/templates/output_popup.html b/app/templates/output_popup.html index 4074977..4d63b11 100644 --- a/app/templates/output_popup.html +++ b/app/templates/output_popup.html @@ -2,4 +2,4 @@

{{ output.title }}

{{output.abstract}}

- \ No newline at end of file + diff --git a/app/templates/outputs.html b/app/templates/outputs.html index b98bf95..4a4b916 100644 --- a/app/templates/outputs.html +++ b/app/templates/outputs.html @@ -8,4 +8,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/pagination.html b/app/templates/pagination.html index 2430f13..ae68bb0 100644 --- a/app/templates/pagination.html +++ b/app/templates/pagination.html @@ -22,4 +22,4 @@ {% endif %} - \ No newline at end of file + diff --git a/app/templates/workstream.html b/app/templates/workstream.html index 3ae1e0a..8272453 100644 --- a/app/templates/workstream.html +++ b/app/templates/workstream.html @@ -10,4 +10,4 @@

{{ name }}

{% include 'author_list.html' %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/workstream_list.html b/app/templates/workstream_list.html index d0bfdb7..24ad205 100644 --- a/app/templates/workstream_list.html +++ b/app/templates/workstream_list.html @@ -1,3 +1,3 @@ {% for workstream in workstreams %} {{ workstream.name }} -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/app/test_html.py b/app/test_html.py index 4a36928..d62156d 100644 --- a/app/test_html.py +++ b/app/test_html.py @@ -37,7 +37,6 @@ def test_output_error_not_exist(self): assert response.status_code == 404 - class TestAuthor: def test_author_list(self): @@ -120,8 +119,7 @@ def test_country_detail_limit_illegal(self): assert response.status_code == 422 def test_country_error_on_not_exist(self): - """Meets - """ + """Meets""" response = client.get("/countries/XXX") assert response.status_code == 404 @@ -133,7 +131,6 @@ def test_workstream_list(self): assert response.status_code == 200 def test_workstream_error_on_not_exist(self): - """Meets - """ + """Meets""" response = client.get("/workstreams/XXX") assert response.status_code == 404 diff --git a/app/test_main.py b/app/test_main.py index 72b4666..72abfef 100644 --- a/app/test_main.py +++ b/app/test_main.py @@ -110,8 +110,7 @@ def test_country_detail_limit_illegal(self): assert response.status_code == 422 def test_country_error_on_not_exist(self): - """Meets - """ + """Meets""" response = client.get("/api/countries/XXX") assert response.status_code == 404 @@ -139,34 +138,42 @@ def test_workstream_list_skip_illegal(self): assert response.status_code == 422 def test_workstream_error_on_not_exist(self): - """Meets - """ + """Meets""" response = client.get("/api/workstreams/XXX") assert response.status_code == 404 + class TestCORS: def test_cors_preflight(self): - response = client.options("/api/authors", headers={ - "Origin": "http://localhost:3000", - "Access-Control-Request-Method": "GET", - "Access-Control-Request-Headers": "Content-Type" - }) + response = client.options( + "/api/authors", + headers={ + "Origin": "http://localhost:3000", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "Content-Type", + }, + ) assert response.status_code == 200 assert response.headers["access-control-allow-origin"] == "*" assert "GET" in response.headers["access-control-allow-methods"] def test_cors_headers_on_response(self): - response = client.get("/api/authors", headers={ - "Origin": "http://localhost:3000" - }) + response = client.get( + "/api/authors", headers={"Origin": "http://localhost:3000"} + ) assert response.status_code == 200 assert response.headers["access-control-allow-origin"] == "*" def test_cors_credentials(self): - response = client.options("/api/authors", headers={ - "Origin": "http://localhost:3000", - "Access-Control-Request-Method": "GET", - "Access-Control-Request-Headers": "Content-Type, Authorization" - }) - assert response.status_code == 200 - assert "authorization" in response.headers["access-control-allow-headers"].lower() \ No newline at end of file + response = client.options( + "/api/authors", + headers={ + "Origin": "http://localhost:3000", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "Content-Type, Authorization", + }, + ) + assert response.status_code == 200 + assert ( + "authorization" in response.headers["access-control-allow-headers"].lower() + ) diff --git a/authors.ttl b/authors.ttl index ceb9d30..e2176c0 100644 --- a/authors.ttl +++ b/authors.ttl @@ -1615,4 +1615,3 @@ , ; org:unitOf . - diff --git a/compose.yaml b/compose.yaml index f241722..63d8e62 100644 --- a/compose.yaml +++ b/compose.yaml @@ -47,4 +47,4 @@ volumes: mg_lib: mg_log: mg_etc: - ri_log: \ No newline at end of file + ri_log: diff --git a/requirements.txt b/requirements.txt index 01ce363..8d7b5f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ exceptiongroup fastapi fastapi-cli Flask +gunicorn h11 httpcore httptools @@ -28,6 +29,7 @@ python-dotenv python-multipart pytz PyYAML +git+https://github.com/ClimateCompatibleGrowth/research_index_backend.git#egg=research_index_backend rich shellingham sniffio @@ -35,9 +37,7 @@ starlette typer typing_extensions uvicorn -gunicorn uvloop watchfiles websockets Werkzeug -git+https://github.com/ClimateCompatibleGrowth/research_index_backend.git#egg=research_index_backend