Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

341 create endpoints for maatregelen and vereisten #67

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,5 @@ index.json

# Logging files
*.log

.DS_Store
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ repos:
rev: v0.7.2
hooks:
- id: ruff
args: [--config, pyproject.toml]
- id: ruff-format
args: [--config, pyproject.toml]
- repo: local
hooks:
- id: validate-schema
Expand Down
1,051 changes: 538 additions & 513 deletions poetry.lock

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ readme = "README.md"
description = ""
authors = ["ai-validatie-team <[email protected]>"]
repository = "https://github.com/MinBZK/task-registry"
keywords = ["AI", "Validation", "Instrument", "Task", "Registry"]
keywords = ["AI", "Validation", "Instrument", "Requirement", "Measure", "Task", "Registry"]
license = "EUPL-1.2"
classifiers = [
"Development Status :: Alpha",
"Framework :: FastAPI",
"Topic :: Software Development :: Libraries :: Python Modules",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Typing :: Typed"
]
packages = [
{ include = "task_registry" }
]
Expand Down Expand Up @@ -48,7 +56,7 @@ types-pyyaml = "^6.0.12.20240724"
# Ruff settings: https://docs.astral.sh/ruff/configuration/
[tool.ruff]
line-length = 120
target-version = "py311"
target-version = "py312"
src = ["task_registry", "tests", "script"]
include = ['script/validate']

Expand Down
5 changes: 3 additions & 2 deletions task_registry/api/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from fastapi import APIRouter
from task_registry.api.routes import health, instruments, urns
from task_registry.api.routes import health, instruments, measures, requirements

api_router = APIRouter()
api_router.include_router(health.router, prefix="/health", tags=["health"])
api_router.include_router(instruments.router, prefix="/instruments", tags=["instruments"])
api_router.include_router(urns.router, prefix="/urns", tags=["urns"])
api_router.include_router(measures.router, prefix="/measures", tags=["measures"])
api_router.include_router(requirements.router, prefix="/requirements", tags=["requirements"])
33 changes: 27 additions & 6 deletions task_registry/api/routes/instruments.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import logging

from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from task_registry.lifespan import CACHED_DATA
from task_registry.data import Index, TaskType
from task_registry.lifespan import CACHED_REGISTRY

router = APIRouter()

Expand All @@ -11,9 +12,29 @@

@router.get(
"/",
summary="Overview of all the Instruments in the Task Registry",
description="This endpoint returns a JSON with all the Instruments in the Task Registry.",
summary="Overview of all the instruments in the task registry.",
description="This endpoint returns a JSON with all the instruments in the task registry.",
responses={200: {"description": "JSON with all the instruments."}},
)
async def get_root() -> JSONResponse:
return JSONResponse(content=CACHED_DATA["index"])
async def get_instruments() -> Index:
return CACHED_REGISTRY.get_tasks_index(TaskType.INSTRUMENTS)


# Optional parameter 'version' is included, but not used. In a new ticket
# versioning of instruments should be handled.
@router.get(
"/urn/{urn}",
summary="Get the contents of the specific instrument which has given URN.",
description="This endpoint returns a JSON with the contents of a specific instrument identified by URN"
" and version.",
responses={
200: {"description": "JSON with the specific contents of the instrument."},
400: {"description": "The URN does not exist or is not valid."},
},
)
async def get_instrument(urn: str, version: str = "latest") -> JSONResponse:
try:
content = CACHED_REGISTRY.get_task(urn, TaskType.INSTRUMENTS)
return JSONResponse(content=content)
except KeyError as err:
raise HTTPException(status_code=400, detail=f"invalid urn: {urn}") from err
40 changes: 40 additions & 0 deletions task_registry/api/routes/measures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import logging

from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from task_registry.data import Index, TaskType
from task_registry.lifespan import CACHED_REGISTRY

router = APIRouter()

logger = logging.getLogger(__name__)


@router.get(
"/",
summary="Overview of all the measures in the task registry",
description="This endpoint returns a JSON with all the measures in the task registry.",
responses={200: {"description": "JSON with all the measures."}},
)
async def get_measures() -> Index:
return CACHED_REGISTRY.get_tasks_index(TaskType.MEASURES)


# Optional parameter 'version' is included, but not used. In a new ticket
# versioning of measures should be handled.
@router.get(
"/urn/{urn}",
summary="Get the contents of the specific measure by URN",
description="This endpoint returns a JSON with the contents of a specific measure identified by URN"
" and version.",
responses={
200: {"description": "JSON with the specific contents of the measure."},
400: {"description": "The URN does not exist or is not valid."},
},
)
async def get_measure(urn: str, version: str = "latest") -> JSONResponse:
try:
content = CACHED_REGISTRY.get_task(urn, TaskType.MEASURES)
return JSONResponse(content=content)
except KeyError as err:
raise HTTPException(status_code=400, detail=f"invalid urn: {urn}") from err
40 changes: 40 additions & 0 deletions task_registry/api/routes/requirements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import logging

from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from task_registry.data import Index, TaskType
from task_registry.lifespan import CACHED_REGISTRY

router = APIRouter()

logger = logging.getLogger(__name__)


@router.get(
"/",
summary="Overview of all the requirements in the task registry.",
description="This endpoint returns a JSON with all the requirements in the task registry.",
responses={200: {"description": "JSON with all the requirements."}},
)
async def get_requirements() -> Index:
return CACHED_REGISTRY.get_tasks_index(TaskType.REQUIREMENTS)


# Optional parameter 'version' is included, but not used. In a new ticket
# versioning of requirements should be handled.
@router.get(
"/urn/{urn}",
summary="Get the contents of the specific instrument which has given URN.",
description="This endpoint returns a JSON with the contents of a specific instrument identified by URN"
" and version.",
responses={
200: {"description": "JSON with the specific contents of the instrument."},
400: {"description": "The URN does not exist or is not valid."},
},
)
async def get_requirement(urn: str, version: str = "latest") -> JSONResponse:
try:
content = CACHED_REGISTRY.get_task(urn, TaskType.REQUIREMENTS)
return JSONResponse(content=content)
except KeyError as err:
raise HTTPException(status_code=400, detail=f"invalid urn: {urn}") from err
31 changes: 0 additions & 31 deletions task_registry/api/routes/urns.py

This file was deleted.

9 changes: 7 additions & 2 deletions task_registry/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@
# Self type is not available in Python 3.10 so create our own with TypeVar
SelfSettings = TypeVar("SelfSettings", bound="Settings")

PROJECT_NAME: str = "TR"
PROJECT_DESCRIPTION: str = "Task Registry"
PROJECT_NAME: str = "Task Registry"
PROJECT_SUMMARY: str = """
API service for the task registry. This API can be used to retrieve
measures, requirements and instruments in this registry.
"""
VERSION: str = "0.1.0" # replace in CI/CD pipeline
LICENSE_NAME: str = "EUPL-1.2 license"
LICENSE_URL: str = "https://eupl.eu/1.2/en/"


class Settings(BaseSettings):
Expand Down
138 changes: 93 additions & 45 deletions task_registry/data.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,115 @@
import logging
import os
from pathlib import Path
from enum import StrEnum
from typing import Any

import yaml
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)


def get_file_size(file_path: str) -> int:
return os.path.getsize(file_path) # pragma: no cover
class TaskType(StrEnum):
INSTRUMENTS = "instruments"
REQUIREMENTS = "requirements"
MEASURES = "measures"


def create_urn_mappper(entries: list[dict[str, Any]]) -> dict[str, Any]:
urn_mapper: dict[str, Any] = {}
for instrument in entries:
path = Path(instrument["path"])
try:
with open(str(path)) as f:
urn_mapper[instrument["urn"]] = yaml.safe_load(f)
except FileNotFoundError:
logger.exception(f"Instrument file with path {path} not found.") # pragma: no cover
class Link(BaseModel):
self: str

except yaml.YAMLError:
logger.exception(f"Instrument file with path {path} could not be parsed.") # pragma: no cover

return urn_mapper
class FileInfo(BaseModel):
type: str
size: int
name: str
path: str
urn: str
download_url: str
links: Link


class Index(BaseModel):
type: str = Field(examples=["dir"])
size: int = Field(examples=[0])
name: str = Field(examples=["task_collection_name"])
path: str = Field(examples=["task_collection_path"])
download_url: str = Field(examples=["https://task-registry.apps.digilab.network/task_collection"])
links: Link = Field(examples=[{"self": "https://task-registry.apps.digilab.network"}])
entries: list[FileInfo] = Field(
examples=[
{
"type": "file",
"size": 1024,
"name": "task_name.yaml",
"path": "task_collection_path/task_name.yaml",
"urn": "urn:nl:aivt:tr:xx:xx",
"download_url": "https://task-registry.apps.digilab.network/task_collection/urn/urn:nl:aivt:tr:aiia:1.0",
"links": {
"self": "https://task-registry.apps.digilab.network/task_collection/urn/urn:nl:aivt:tr:aiia:1.0"
},
}
]
)


class CachedRegistry:
def __init__(self) -> None:
self.index_cache: dict[TaskType, Index] = {}
self.tasks_cache: dict[tuple[str, TaskType], Any] = {}

def add_tasks(self, tasks: TaskType) -> None:
index = generate_index(tasks)
self.index_cache[tasks] = index

for task in index.entries:
try:
with open(task.path) as f:
self.tasks_cache[(task.urn, tasks)] = yaml.safe_load(f)
except FileNotFoundError:
logger.exception(f"Task file with path {task.path} not found.") # pragma: no cover
except yaml.YAMLError:
logger.exception(f"Task file with path {task.path} could not be parsed.") # pragma: no cover

def get_tasks_index(self, tasks: TaskType) -> Index:
return self.index_cache[tasks]

def get_task(self, urn: str, tasks: TaskType) -> dict[str, Any]:
return self.tasks_cache[(urn, tasks)]


def generate_index(
ChristopherSpelt marked this conversation as resolved.
Show resolved Hide resolved
tasks: TaskType,
base_url: str = "https://task-registry.apps.digilab.network",
directory: str = "instruments",
) -> dict[str, Any]:
index: dict[str, Any] = {
"type": "dir",
"size": 0,
"name": directory,
"path": directory,
"download_url": f"{base_url}/instruments",
"_links": {
"self": f"{base_url}/instruments",
},
"entries": [],
}

for root, _, files in os.walk(directory):
) -> Index:
tasks_url = f"{base_url}/{tasks}"
entries: list[FileInfo] = []

for root, _, files in os.walk(tasks):
for file in files:
if file.endswith(".yaml"):
file_path = os.path.join(root, file)
relative_path = file_path.replace("\\", "/")
with open(file_path) as f:
instrument = yaml.safe_load(f)
file_info = {
"type": "file",
"size": get_file_size(file_path),
"name": file,
"path": relative_path,
"urn": instrument["urn"],
"download_url": f"{base_url}/urns/?urn={instrument['urn']}",
"_links": {
"self": f"{base_url}/urns/?urn={instrument['urn']}",
},
}
index["entries"].append(file_info)

return index
task = yaml.safe_load(f)
task_url = f"{tasks_url}/urn/{task['urn']}"
file_info = FileInfo(
type="file",
size=os.path.getsize(file_path),
name=file,
path=relative_path,
urn=task["urn"],
download_url=task_url,
links=Link(self=task_url),
)
entries.append(file_info)

return Index(
type="dir",
size=0,
name=tasks.value,
path=tasks.value,
download_url=tasks_url,
links=Link(self=tasks_url),
entries=entries,
)
Loading
Loading