Skip to content

Commit 8080e1d

Browse files
committed
add test and ci
1 parent 449e65e commit 8080e1d

14 files changed

+699
-16
lines changed

.github/codecov.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
comment: off
2+
3+
coverage:
4+
status:
5+
project:
6+
default:
7+
target: auto
8+
threshold: 5

.github/dependabot.yml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Set update schedule for GitHub Actions
2+
3+
version: 2
4+
updates:
5+
6+
- package-ecosystem: "github-actions"
7+
directory: "/"
8+
schedule:
9+
# Check for updates to GitHub Actions every week
10+
interval: "weekly"
11+
groups:
12+
all:
13+
patterns:
14+
- "*"

.github/workflows/ci.yml

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: CI
2+
3+
# On every pull request, but only on push to main
4+
on:
5+
push:
6+
branches:
7+
- main
8+
tags:
9+
- '*'
10+
paths:
11+
# Only run test and docker publish if somde code have changed
12+
- 'pyproject.toml'
13+
- 'setup.py'
14+
- 'stac_fastapi/**'
15+
- 'tests/**'
16+
- '.pre-commit-config.yaml'
17+
- '.github/workflows/ci.yml'
18+
pull_request:
19+
env:
20+
LATEST_PY_VERSION: '3.13'
21+
22+
jobs:
23+
tests:
24+
runs-on: ubuntu-latest
25+
strategy:
26+
matrix:
27+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
28+
29+
steps:
30+
- uses: actions/checkout@v4
31+
- name: Set up Python ${{ matrix.python-version }}
32+
uses: actions/setup-python@v5
33+
with:
34+
python-version: ${{ matrix.python-version }}
35+
36+
- name: Install dependencies
37+
run: |
38+
python -m pip install --upgrade pip
39+
python -m pip install .["test"]
40+
41+
- name: Run pre-commit
42+
if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
43+
run: |
44+
python -m pip install pre-commit
45+
pre-commit run --all-files
46+
47+
- name: Run tests
48+
run: python -m pytest --cov stac_fastapi_html --cov-report xml --cov-report term-missing --asyncio-mode=strict
49+
50+
- name: Upload Results
51+
if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
52+
uses: codecov/codecov-action@v5
53+
with:
54+
file: ./coverage.xml
55+
flags: unittests
56+
name: ${{ matrix.python-version }}
57+
fail_ci_if_error: false

setup.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@
66
desc = f.read()
77

88
install_requires = [
9-
"stac-fastapi.api",
9+
"starlette",
1010
"jinja2>=2.11.2,<4.0.0",
1111
]
1212

1313
extra_reqs = {
14-
"dev": [
14+
"test": [
1515
"pytest",
1616
"pytest-cov",
1717
"pytest-asyncio",
1818
"httpx",
19+
],
20+
"dev": [
1921
"pre-commit",
22+
"bump-my-version",
2023
],
2124
}
2225

stac_fastapi/html/middleware.py

+23-14
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
"""stac-fastapi HTML middlewares."""
22

3+
from __future__ import annotations
4+
35
import json
46
import re
57
from dataclasses import dataclass, field
6-
from typing import Any, List, Optional
8+
from typing import TYPE_CHECKING, Any, List, Optional
79

810
import jinja2
9-
from stac_pydantic.shared import MimeTypes
1011
from starlette.datastructures import MutableHeaders
1112
from starlette.requests import Request
1213
from starlette.templating import Jinja2Templates
13-
from starlette.types import ASGIApp, Message, Receive, Scope, Send
14+
15+
if TYPE_CHECKING:
16+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
1417

1518
jinja2_env = jinja2.Environment(
1619
loader=jinja2.ChoiceLoader(
@@ -46,6 +49,9 @@ def preferred_encoding(accept: str) -> Optional[List[str]]:
4649
"""
4750
accept_values = {}
4851
for m in accept.replace(" ", "").split(","):
52+
if not m:
53+
continue
54+
4955
values = m.split(";")
5056
if len(values) == 1:
5157
name = values[0]
@@ -78,6 +84,7 @@ class HTMLRenderMiddleware:
7884

7985
app: ASGIApp
8086
templates: Jinja2Templates = field(default_factory=lambda: DEFAULT_TEMPLATES)
87+
endpoints_names: dict[str, str] = field(default_factory=lambda: ENDPOINT_TEMPLATES)
8188

8289
def create_html_response(
8390
self,
@@ -89,7 +96,7 @@ def create_html_response(
8996
**kwargs: Any,
9097
) -> bytes:
9198
"""Create Template response."""
92-
router_prefix = request.app.state.router_prefix
99+
router_prefix = getattr(request.app.state, "router_prefix", None)
93100

94101
urlpath = request.url.path
95102
if root_path := request.app.root_path:
@@ -167,23 +174,25 @@ async def send_as_html(message: Message):
167174
request = Request(scope, receive=receive)
168175
pref_encoding = preferred_encoding(request.headers.get("accept", "")) or []
169176

170-
output_type: Optional[MimeTypes] = None
177+
encode_to_html = False
171178
if request.query_params.get("f", "") == "html":
172-
output_type = MimeTypes.html
173-
elif "text/html" in pref_encoding and not request.query_params.get("f", ""):
174-
output_type = MimeTypes.html
175-
176-
if start_message["status"] == 200 and output_type:
177-
headers = MutableHeaders(scope=start_message)
178-
if tpl := ENDPOINT_TEMPLATES.get(scope["route"].name):
179-
headers["content-type"] = "text/html"
179+
encode_to_html = True
180+
elif (
181+
"text/html" in pref_encoding or "*" in pref_encoding
182+
) and not request.query_params.get("f", ""):
183+
encode_to_html = True
184+
185+
if start_message["status"] == 200 and encode_to_html:
186+
# NOTE: `scope["route"]` seems to be specific to FastAPI application
187+
if tpl := self.endpoints_names.get(scope["route"].name):
180188
body = self.create_html_response(
181189
request,
182190
json.loads(body.decode()),
183191
template_name=tpl,
184192
title=scope["route"].name,
185193
)
186-
headers["Content-Encoding"] = "text/html"
194+
headers = MutableHeaders(scope=start_message)
195+
headers["Content-Type"] = "text/html"
187196
headers["Content-Length"] = str(len(body))
188197

189198
# Send http.response.start

tests/conftest.py

Whitespace-only changes.
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"$id": "http://127.0.0.1:8000//collections/my_collection/queryables",
3+
"type": "object",
4+
"title": "STAC Queryables.",
5+
"$schema": "http://json-schema.org/draft-07/schema#",
6+
"properties": {
7+
"id": {
8+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id",
9+
"title": "Item ID",
10+
"description": "Item identifier"
11+
},
12+
"datetime": {
13+
"type": "string",
14+
"title": "Acquired",
15+
"format": "date-time",
16+
"pattern": "(+00:00|Z)$",
17+
"description": "Datetime"
18+
},
19+
"geometry": {
20+
"$ref": "https://geojson.org/schema/Feature.json",
21+
"title": "Item Geometry",
22+
"description": "Item Geometry"
23+
}
24+
},
25+
"additionalProperties": true
26+
}

tests/fixtures/collections.json

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"collections": [
3+
{
4+
"id": "my_collection",
5+
"type": "Collection",
6+
"links": [
7+
{
8+
"rel": "items",
9+
"type": "application/geo+json",
10+
"href": "http://127.0.0.1:8000/collections/my_collection/items"
11+
},
12+
{
13+
"rel": "parent",
14+
"type": "application/json",
15+
"href": "http://127.0.0.1:8000/"
16+
},
17+
{
18+
"rel": "root",
19+
"type": "application/json",
20+
"href": "http://127.0.0.1:8000/"
21+
},
22+
{
23+
"rel": "self",
24+
"type": "application/json",
25+
"href": "http://127.0.0.1:8000/collections/my_collection"
26+
},
27+
{
28+
"rel": "license",
29+
"href": "https://creativecommons.org/licenses/publicdomain/",
30+
"title": "public domain"
31+
},
32+
{
33+
"rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables",
34+
"type": "application/schema+json",
35+
"title": "Queryables",
36+
"href": "http://127.0.0.1:8000/collections/my_collection/queryables"
37+
}
38+
],
39+
"extent": {
40+
"spatial": {
41+
"bbox": [[]]
42+
},
43+
"temporal": {
44+
"interval": [[]]
45+
}
46+
},
47+
"license": "public-domain",
48+
"description": "My Collection",
49+
"stac_version": "1.0.0"
50+
}
51+
],
52+
"links": [
53+
{
54+
"rel": "root",
55+
"type": "application/json",
56+
"href": "http://127.0.0.1:8000/"
57+
},
58+
{
59+
"rel": "self",
60+
"type": "application/json",
61+
"href": "http://127.0.0.1:8000/collections"
62+
}
63+
],
64+
"numberReturned": 1
65+
}

tests/fixtures/conformances.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"conformsTo": [
3+
"http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2",
4+
"http://www.opengis.net/spec/cql2/1.0/conf/cql2-json",
5+
"http://www.opengis.net/spec/cql2/1.0/conf/cql2-text",
6+
"http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query",
7+
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
8+
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
9+
"http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30",
10+
"http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter",
11+
"http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter",
12+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search",
13+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields",
14+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter",
15+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text",
16+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#query",
17+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort",
18+
"https://api.stacspec.org/v1.0.0-rc.2/item-search#filter",
19+
"https://api.stacspec.org/v1.0.0/collections",
20+
"https://api.stacspec.org/v1.0.0/collections/extensions/transaction",
21+
"https://api.stacspec.org/v1.0.0/core",
22+
"https://api.stacspec.org/v1.0.0/item-search",
23+
"https://api.stacspec.org/v1.0.0/item-search#fields",
24+
"https://api.stacspec.org/v1.0.0/item-search#query",
25+
"https://api.stacspec.org/v1.0.0/item-search#sort",
26+
"https://api.stacspec.org/v1.0.0/ogcapi-features",
27+
"https://api.stacspec.org/v1.0.0/ogcapi-features#fields",
28+
"https://api.stacspec.org/v1.0.0/ogcapi-features#query",
29+
"https://api.stacspec.org/v1.0.0/ogcapi-features#sort",
30+
"https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction"
31+
]
32+
}

0 commit comments

Comments
 (0)