Skip to content
Draft
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
31 changes: 31 additions & 0 deletions felt_python/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json as json_
import os
import typing
import urllib.parse
import urllib.request
from importlib.metadata import version, PackageNotFoundError

Expand All @@ -20,6 +21,36 @@
BASE_URL = os.getenv("FELT_BASE_URL", "https://felt.com/api/v2/")


def build_url(template: str, **path_params) -> str:
"""Fill a URL template, percent-encoding each value as one path segment.

Ids reach these functions from user code, config files and other API
responses, so they cannot be assumed URL-safe. Interpolating them directly
means an id containing a space raises http.client.InvalidURL before the
request is sent, and one containing "?", "#" or "/" silently changes the
path or query the server sees. Encoding each value with safe="" keeps a
bad id a plain 404.

>>> build_url(BASE_URL + "maps/{map_id}", map_id="a b/c")
'https://felt.com/api/v2/maps/a%20b%2Fc'
"""
return template.format(
**{
key: urllib.parse.quote(str(value), safe="")
for key, value in path_params.items()
}
)


def build_query(url: str, **params) -> str:
"""Append a percent-encoded query string, skipping None values."""
present = {key: value for key, value in params.items() if value is not None}
if not present:
return url
separator = "&" if "?" in url else "?"
return f"{url}{separator}{urllib.parse.urlencode(present)}"


def make_request(
url: str,
method: typing.Literal["GET", "POST", "PATCH", "DELETE"],
Expand Down
8 changes: 4 additions & 4 deletions felt_python/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from urllib.parse import urljoin

from .api import make_request, BASE_URL
from .api import BASE_URL, build_query, build_url, make_request


COMMENT = urljoin(BASE_URL, "maps/{map_id}/comments/{comment_id}")
Expand All @@ -23,7 +23,7 @@ def export_comments(map_id: str, format: str = "json", api_token: str | None = N
Returns:
The exported comments in the specified format
"""
url = f"{COMMENT_EXPORT.format(map_id=map_id)}?format={format}"
url = build_query(build_url(COMMENT_EXPORT, map_id=map_id), format=format)
response = make_request(
url=url,
method="GET",
Expand All @@ -44,7 +44,7 @@ def resolve_comment(map_id: str, comment_id: str, api_token: str | None = None):
Confirmation of the resolved comment
"""
response = make_request(
url=COMMENT_RESOLVE.format(map_id=map_id, comment_id=comment_id),
url=build_url(COMMENT_RESOLVE, map_id=map_id, comment_id=comment_id),
method="POST",
api_token=api_token,
)
Expand All @@ -60,7 +60,7 @@ def delete_comment(map_id: str, comment_id: str, api_token: str | None = None):
api_token: Optional API token
"""
make_request(
url=COMMENT.format(map_id=map_id, comment_id=comment_id),
url=build_url(COMMENT, map_id=map_id, comment_id=comment_id),
method="DELETE",
api_token=api_token,
)
14 changes: 7 additions & 7 deletions felt_python/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from urllib.parse import urljoin

from .api import make_request, BASE_URL
from .api import BASE_URL, build_url, make_request
from .util import deprecated


Expand All @@ -25,7 +25,7 @@ def list_elements(map_id: str, api_token: str | None = None):
GeoJSON FeatureCollection of all elements
"""
response = make_request(
url=ELEMENTS.format(map_id=map_id),
url=build_url(ELEMENTS, map_id=map_id),
method="GET",
api_token=api_token,
)
Expand All @@ -43,7 +43,7 @@ def list_element_groups(map_id: str, api_token: str | None = None):
List of element groups
"""
response = make_request(
url=ELEMENT_GROUPS.format(map_id=map_id),
url=build_url(ELEMENT_GROUPS, map_id=map_id),
method="GET",
api_token=api_token,
)
Expand All @@ -62,7 +62,7 @@ def get_element_group(map_id: str, element_group_id: str, api_token: str | None
GeoJSON FeatureCollection of all elements in the group
"""
response = make_request(
url=ELEMENT_GROUP.format(map_id=map_id, element_group_id=element_group_id),
url=build_url(ELEMENT_GROUP, map_id=map_id, element_group_id=element_group_id),
method="GET",
api_token=api_token,
)
Expand Down Expand Up @@ -106,7 +106,7 @@ def upsert_elements(
"geojson_feature_collection must be a valid GeoJSON"
)
response = make_request(
url=ELEMENTS.format(map_id=map_id),
url=build_url(ELEMENTS, map_id=map_id),
method="POST",
json=geojson_feature_collection,
api_token=api_token,
Expand All @@ -123,7 +123,7 @@ def delete_element(map_id: str, element_id: str, api_token: str | None = None):
api_token: Optional API token
"""
make_request(
url=ELEMENT.format(map_id=map_id, element_id=element_id),
url=build_url(ELEMENT, map_id=map_id, element_id=element_id),
method="DELETE",
api_token=api_token,
)
Expand Down Expand Up @@ -159,7 +159,7 @@ def upsert_element_groups(
The created or updated element groups
"""
response = make_request(
url=ELEMENT_GROUPS.format(map_id=map_id),
url=build_url(ELEMENT_GROUPS, map_id=map_id),
method="POST",
json=element_groups,
api_token=api_token,
Expand Down
14 changes: 7 additions & 7 deletions felt_python/layer_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from urllib.parse import urljoin

from .api import make_request, BASE_URL
from .api import BASE_URL, build_url, make_request


GROUPS = urljoin(BASE_URL, "maps/{map_id}/layer_groups")
Expand All @@ -25,7 +25,7 @@ def list_layer_groups(map_id: str, api_token: str | None = None):
List of layer groups
"""
response = make_request(
url=GROUPS.format(map_id=map_id),
url=build_url(GROUPS, map_id=map_id),
method="GET",
api_token=api_token,
)
Expand All @@ -48,7 +48,7 @@ def get_layer_group(
Layer group details
"""
response = make_request(
url=GROUP.format(map_id=map_id, layer_group_id=layer_group_id),
url=build_url(GROUP, map_id=map_id, layer_group_id=layer_group_id),
method="GET",
api_token=api_token,
)
Expand All @@ -73,7 +73,7 @@ def update_layer_groups(
The updated layer groups
"""
response = make_request(
url=GROUPS.format(map_id=map_id),
url=build_url(GROUPS, map_id=map_id),
method="POST",
json=layer_group_params_list,
api_token=api_token,
Expand All @@ -94,7 +94,7 @@ def delete_layer_group(
api_token: Optional API token
"""
make_request(
url=GROUP.format(map_id=map_id, layer_group_id=layer_group_id),
url=build_url(GROUP, map_id=map_id, layer_group_id=layer_group_id),
method="DELETE",
api_token=api_token,
)
Expand Down Expand Up @@ -136,7 +136,7 @@ def update_layer_group(
json_payload["visibility_interaction"] = visibility_interaction

response = make_request(
url=GROUP.format(map_id=map_id, layer_group_id=layer_group_id),
url=build_url(GROUP, map_id=map_id, layer_group_id=layer_group_id),
method="POST",
json=json_payload,
api_token=api_token,
Expand Down Expand Up @@ -166,7 +166,7 @@ def publish_layer_group(
json_payload["name"] = name

response = make_request(
url=GROUPS_PUBLISH.format(map_id=map_id, layer_group_id=layer_group_id),
url=build_url(GROUPS_PUBLISH, map_id=map_id, layer_group_id=layer_group_id),
method="POST",
json=json_payload,
api_token=api_token,
Expand Down
32 changes: 18 additions & 14 deletions felt_python/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from urllib.parse import urljoin

from .api import make_request, BASE_URL
from .api import BASE_URL, build_url, make_request
from .util import deprecated


Expand All @@ -31,7 +31,7 @@
def list_layers(map_id: str, api_token: str | None = None):
"""List layers on a map"""
response = make_request(
url=LAYERS.format(map_id=map_id),
url=build_url(LAYERS, map_id=map_id),
method="GET",
api_token=api_token,
)
Expand Down Expand Up @@ -79,7 +79,7 @@ def upload_file(
json_payload["zoom"] = zoom

response = make_request(
url=LAYER_UPLOAD.format(map_id=map_id),
url=build_url(LAYER_UPLOAD, map_id=map_id),
method="POST",
api_token=api_token,
json=json_payload,
Expand Down Expand Up @@ -147,7 +147,7 @@ def refresh_file_layer(
The refresh response including presigned upload details
"""
response = make_request(
url=LAYER_REFRESH.format(map_id=map_id, layer_id=layer_id),
url=build_url(LAYER_REFRESH, map_id=map_id, layer_id=layer_id),
method="POST",
api_token=api_token,
)
Expand Down Expand Up @@ -186,7 +186,7 @@ def upload_url(
json_payload["hints"] = hints

response = make_request(
url=LAYER_UPLOAD.format(map_id=map_id),
url=build_url(LAYER_UPLOAD, map_id=map_id),
method="POST",
api_token=api_token,
json=json_payload,
Expand All @@ -197,7 +197,8 @@ def upload_url(
def refresh_url_layer(map_id: str, layer_id: str, api_token: str | None = None):
"""Refresh a layer originated from a URL upload"""
response = make_request(
url=LAYER_REFRESH.format(
url=build_url(
LAYER_REFRESH,
map_id=map_id,
layer_id=layer_id,
),
Expand All @@ -219,7 +220,8 @@ def get_layer(
):
"""Get details of a layer"""
response = make_request(
url=LAYER.format(
url=build_url(
LAYER,
map_id=map_id,
layer_id=layer_id,
),
Expand All @@ -237,7 +239,8 @@ def update_layer_style(
):
"""Update a layer's style"""
response = make_request(
url=LAYER_UPDATE_STYLE.format(
url=build_url(
LAYER_UPDATE_STYLE,
map_id=map_id,
layer_id=layer_id,
),
Expand All @@ -258,7 +261,7 @@ def get_export_link(
Vector layers will be downloaded in GPKG format. Raster layers will be GeoTIFFs.
"""
response = make_request(
url=LAYER_EXPORT_LINK.format(map_id=map_id, layer_id=layer_id),
url=build_url(LAYER_EXPORT_LINK, map_id=map_id, layer_id=layer_id),
method="GET",
api_token=api_token,
)
Expand Down Expand Up @@ -306,7 +309,7 @@ def update_layers(
The updated layers
"""
response = make_request(
url=LAYERS.format(map_id=map_id),
url=build_url(LAYERS, map_id=map_id),
method="POST",
json=layer_params_list,
api_token=api_token,
Expand All @@ -321,7 +324,7 @@ def delete_layer(
):
"""Delete a layer from a map"""
make_request(
url=LAYER.format(map_id=map_id, layer_id=layer_id),
url=build_url(LAYER, map_id=map_id, layer_id=layer_id),
method="DELETE",
api_token=api_token,
)
Expand Down Expand Up @@ -349,7 +352,7 @@ def publish_layer(
json_payload["name"] = name

response = make_request(
url=LAYER_PUBLISH.format(map_id=map_id, layer_id=layer_id),
url=build_url(LAYER_PUBLISH, map_id=map_id, layer_id=layer_id),
method="POST",
json=json_payload,
api_token=api_token,
Expand Down Expand Up @@ -389,7 +392,7 @@ def create_custom_export(
json_payload["filters"] = filters

response = make_request(
url=LAYER_CUSTOM_EXPORT.format(map_id=map_id, layer_id=layer_id),
url=build_url(LAYER_CUSTOM_EXPORT, map_id=map_id, layer_id=layer_id),
method="POST",
json=json_payload,
api_token=api_token,
Expand All @@ -415,7 +418,8 @@ def get_custom_export_status(
Export status including download URL when complete
"""
response = make_request(
url=LAYER_CUSTOM_EXPORT_STATUS.format(
url=build_url(
LAYER_CUSTOM_EXPORT_STATUS,
map_id=map_id,
layer_id=layer_id,
export_id=export_id,
Expand Down
4 changes: 2 additions & 2 deletions felt_python/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from urllib.parse import urljoin

from .api import make_request, BASE_URL
from .api import BASE_URL, build_query, make_request


LIBRARY = urljoin(BASE_URL, "library")
Expand All @@ -24,7 +24,7 @@ def list_library_layers(source: str = "workspace", api_token: str | None = None)
Returns:
The layer library containing layers and layer groups
"""
url = f"{LIBRARY}?source={source}"
url = build_query(LIBRARY, source=source)
response = make_request(
url=url,
method="GET",
Expand Down
Loading
Loading