Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fc7b9d2
add examples to api
ahouseholder Oct 9, 2025
87224c2
add EXAMPLE_MINIMAL_DECISION_POINT_VALUE to examples
ahouseholder Oct 9, 2025
7ecedd3
add test for examples in app
ahouseholder Oct 9, 2025
c20e4a2
add endpoint to retrieve sample references and corresponding tests
ahouseholder Oct 9, 2025
8e59824
mark test_get_selection_lists as expected failure and add TODO for PO…
ahouseholder Oct 9, 2025
17b6845
add POST endpoint to validate Reference and corresponding tests
ahouseholder Oct 10, 2025
3ea12b4
Merge branch 'main' of https://github.com/CERTCC/SSVC into api-examples
ahouseholder Oct 10, 2025
bb41416
remove debug print statement from data cleanup function
ahouseholder Oct 10, 2025
650e391
add tests for SelectionList to validate empty lists and model dumping…
ahouseholder Oct 10, 2025
054261f
refactor examples and tests for clarity and structure
ahouseholder Oct 10, 2025
e2a67bc
add model validators to handle default summary and remove falsy fields
ahouseholder Oct 10, 2025
1c36e52
Merge branch 'main' into api-examples
ahouseholder Oct 10, 2025
20405dc
add prefix and tags to API router and enhance root redirect description
ahouseholder Oct 10, 2025
87041a6
remove prefix and tags from v1 API router configuration
ahouseholder Oct 10, 2025
2a88e43
add README.md for SSVC API setup and usage instructions
ahouseholder Oct 10, 2025
87fb9e0
Merge remote-tracking branch 'gh_pub/api-examples' into api-examples
ahouseholder Oct 10, 2025
f44a2f3
Update src/test/api/routers/test_examples.py
ahouseholder Oct 10, 2025
83161ad
black formatter
ahouseholder Oct 10, 2025
66ed04a
Merge remote-tracking branch 'gh_pub/api-examples' into api-examples
ahouseholder Oct 10, 2025
96b7b53
Update src/ssvc/selection.py
ahouseholder Oct 10, 2025
d29d624
Update src/test/test_selections.py
ahouseholder Oct 10, 2025
695779b
Update src/test/test_selections.py
ahouseholder Oct 10, 2025
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
56 changes: 56 additions & 0 deletions src/ssvc/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# SSVC API Readme

This directory contains source code for the SSVC API.

## Prerequisites

- `uv` CLI tool installed. You can install it via pip:

```shell
pip install uv
```

We recommend using `uv` to manage your Python environment and dependencies,
so you don't need to manually create and activate virtual environments or
worry about Python versions.

## Running a local instance in development mode

From the project root, run:

```shell
uv --project=src run uvicorn ssvc.api.main:app --reload --port=7777
```

> [!TIP]
> Adjust the port as needed.

> [!NOTE]
> We're planning to move our `pyproject.toml` to the top level of the project,
> so in the future you may be able to run this command without the `--project` flag.

This will start the FastAPI server with auto-reload enabled, allowing you to
see changes immediately.

## Running a local instance in production mode

From the project root, run:

```shell
cd docker
docker-compose up api
```

This will start the FastAPI server in a Docker container.

> [!NOTE]
> Docker and Docker Compose must be installed on your machine to use this method.
> Make sure to adjust the `docker-compose.yml` file if you want to change
> the port or other settings.

> [!TIP]
> The `api` docker target copies the code into the container at build time.
> If you make changes to the code, you'll need to rebuild the Docker image
> using `docker-compose build api` before restarting the container. Or else
> use `docker-compose up --build api` to build and start in one command.

5 changes: 2 additions & 3 deletions src/ssvc/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
API for SSVC
"""


# Copyright (c) 2025 Carnegie Mellon University.
# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE
# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS.
Expand Down Expand Up @@ -45,12 +44,12 @@
},
)

app.include_router(router_v1)
app.include_router(router_v1, prefix="/ssvc/api/v1", tags=["SSVC API v1"])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This changes the prefix for the whole API. If we don't want to force the /ssvc/api prefix (or we'd rather it just be /ssvc/v1), we should change this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, instead of having ssvc.api.v1.routers.v1_router.router assign the /v1 part of the prefix, this PR changes it so the /v1 prefix is added by the include_router call. I'm not sure which way is preferable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no preference really. I think it is fine the way you have it.



# root should redirect to docs
# at least until we have something better to show
@app.get("/", include_in_schema=False)
@app.get("/", include_in_schema=False, description="Redirect to API docs")
async def redirect_root_to_docs():
return RedirectResponse(url="/docs")

Expand Down
253 changes: 253 additions & 0 deletions src/ssvc/api/v1/routers/examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
#!/usr/bin/env python
"""
SSVC API v1 Examples Router
"""

# Copyright (c) 2025 Carnegie Mellon University.
# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE
# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS.
# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND,
# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT
# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR
# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE
# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE
# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM
# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
# Licensed under a MIT (SEI)-style license, please see LICENSE or contact
# [email protected] for full terms.
# [DISTRIBUTION STATEMENT A] This material has been approved for
# public release and unlimited distribution. Please see Copyright notice
# for non-US Government use and distribution.
# This Software includes and/or makes use of Third-Party Software each
# subject to its own license.
# DM24-0278

from fastapi import APIRouter

from ssvc.decision_points.base import DecisionPoint, DecisionPointValue
from ssvc.decision_tables.base import DecisionTable
from ssvc.examples import (
EXAMPLE_DECISION_POINT_1,
EXAMPLE_DECISION_TABLE,
EXAMPLE_MINIMAL_DECISION_POINT_VALUE,
EXAMPLE_SELECTION_1,
EXAMPLE_SELECTION_LIST,
)
from ssvc.selection import (
MinimalDecisionPointValue,
Reference,
Selection,
SelectionList,
)

router = APIRouter(prefix="/examples", tags=["Examples"])

# GET to retrieve a sample object
# POST to validate an object against the pydantic model


# Decision Point Values
@router.get(
"/decision-point-values",
response_model=DecisionPointValue,
response_model_exclude_none=True,
summary="Get a sample Decision Point Value",
description="Retrieve a sample Decision Point Value object.",
)
def get_example_decision_point_value() -> DecisionPointValue:
"""
Retrieve a sample Decision Point Value object.
"""
return EXAMPLE_DECISION_POINT_1.values[0]


@router.post(
"/decision-point-values",
response_model=DecisionPointValue,
response_model_exclude_none=True,
summary="Validate a Decision Point Value",
description="Validate a Decision Point Value object against the pydantic model.",
)
def validate_decision_point_value(
decision_point_value: DecisionPointValue,
) -> DecisionPointValue:
"""
Validate a Decision Point Value object against the pydantic model.
"""
return decision_point_value


# Decision Points
@router.get(
"/decision-points",
response_model=DecisionPoint,
response_model_exclude_none=True,
summary="Get a sample Decision Point",
description="Retrieve a sample Decision Point object.",
)
def get_example_decision_point() -> DecisionPoint:
"""
Retrieve a sample Decision Point object.
"""
return EXAMPLE_DECISION_POINT_1


@router.post(
"/decision-points",
response_model=DecisionPoint,
response_model_exclude_none=True,
summary="Validate a Decision Point",
description="Validate a Decision Point object against the pydantic model.",
)
def validate_decision_point(decision_point: DecisionPoint) -> DecisionPoint:
"""
Validate a Decision Point object against the pydantic model.
"""
return decision_point


# Decision Tables
@router.get(
"/decision-tables",
response_model=DecisionTable,
response_model_exclude_none=True,
summary="Get a sample Decision Table",
description="Retrieve a sample Decision Table object.",
)
def get_example_decision_table() -> DecisionTable:
"""
Retrieve a sample Decision Table object.
"""
return EXAMPLE_DECISION_TABLE


@router.post(
"/decision-tables",
response_model=DecisionTable,
response_model_exclude_none=True,
summary="Validate a Decision Table",
description="Validate a Decision Table object against the pydantic model.",
)
def validate_decision_table(decision_table: DecisionTable) -> DecisionTable:
"""
Validate a Decision Table object against the pydantic model.
"""
return decision_table


# minimal decision point values
@router.get(
"/decision-point-values-minimal",
response_model=MinimalDecisionPointValue,
response_model_exclude_none=True,
summary="Get a minimal Decision Point Value",
description="Retrieve a minimal Decision Point Value object.",
)
def get_minimal_decision_point_value() -> MinimalDecisionPointValue:
"""
Retrieve a minimal Decision Point Value object.
"""
return EXAMPLE_MINIMAL_DECISION_POINT_VALUE


@router.post(
"/decision-point-values-minimal",
response_model=MinimalDecisionPointValue,
response_model_exclude_none=True,
summary="Validate a minimal Decision Point Value",
description="Validate a minimal Decision Point Value object against the pydantic model.",
)
def validate_minimal_decision_point_value(
minimal_decision_point_value: MinimalDecisionPointValue,
) -> MinimalDecisionPointValue:
"""
Validate a minimal Decision Point Value object against the pydantic model.
"""
return minimal_decision_point_value


# selection
@router.get(
"/selections",
response_model=Selection,
response_model_exclude_none=True,
summary="Get a sample Selection",
description="Retrieve a sample Selection object.",
)
def get_example_selection() -> Selection:
"""
Retrieve a sample Selection object.
"""
return EXAMPLE_SELECTION_1


@router.post(
"/selections",
response_model=Selection,
response_model_exclude_none=True,
summary="Validate a Selection",
description="Validate a Selection object against the pydantic model.",
)
def validate_selection(selection: Selection) -> Selection:
"""
Validate a Selection object against the pydantic model.
"""
return selection


# Selection lists
@router.get(
"/selection-lists",
response_model=SelectionList,
response_model_exclude_none=True,
summary="Get a sample Selection List",
description="Retrieve a sample Selection List object.",
)
def get_example_selection_list() -> SelectionList:
"""
Retrieve a sample Selection List object.
"""
return EXAMPLE_SELECTION_LIST


@router.post(
"/selection-lists",
response_model=SelectionList,
response_model_exclude_none=True,
summary="Validate a Selection List",
description="Validate a Selection List object against the pydantic model.",
)
def validate_selection_list(selection_list: SelectionList) -> SelectionList:
"""
Validate a Selection List object against the pydantic model.
"""
return selection_list


# references
@router.get(
"/references",
response_model=Reference,
response_model_exclude_none=True,
summary="Get sample References",
description="Retrieve a list of sample Reference URIs.",
)
def get_example_references() -> Reference:
"""
Retrieve a list of sample Reference URIs.
"""
return EXAMPLE_SELECTION_LIST.references[0]


@router.post(
"/references",
response_model=Reference,
response_model_exclude_none=True,
summary="Validate a Reference",
description="Validate a Reference object against the pydantic model.",
)
def validate_reference(reference: Reference) -> Reference:
"""
Validate a Reference object against the pydantic model.
"""
return reference
4 changes: 3 additions & 1 deletion src/ssvc/api/v1/routers/v1_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
decision_point,
decision_table,
decision_tables,
examples,
objects,
)
from ssvc.api.v1.routers import (
Expand All @@ -36,7 +37,8 @@
versions,
)

router_v1 = APIRouter(prefix="/v1", tags=["v1"])
router_v1 = APIRouter()
router_v1.include_router(examples.router)
router_v1.include_router(decision_point.router)
router_v1.include_router(decision_points.router)
router_v1.include_router(decision_table.router)
Expand Down
Loading
Loading