Skip to content
Merged
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ repos:
# E501 let black handle all line length decisions
# W503 black conflicts with "line break before operator" rule
# E203 black conflicts with "whitespace before ':'" rule
# E231 black conflicts with "whitespace after ':'" rule
# E722 bare excepts need to be addressed
'--ignore=E501,W503,E203,E722']
'--ignore=E501,W503,E203,E722,E231']
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

=======

# 1.14.0 (2025-04-23)
- Added command `tilesets upload-changeset` that uploads a changeset file.
- Added command `tilesets publish-changesets` that publishes changesets for a incrementally updatable tileset.
- Added command `tilesets view-changeset` that view an uploaded changeset.
- Added command `tilesets delete-changeset` that deletes an uploaded changeset.

# 1.13.0 (2025-01-07)
- Temporarily remove the `estimate-cu` command for further refinement.

Expand Down
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,12 @@ export MAPBOX_ACCESS_TOKEN=my.token
- Tileset Sources
- [`upload-source`](#upload-source)
- [`upload-raster-source`](#upload-raster-source) (new)
- [`upload-changeset`](#upload-changeset)
- _deprecated_ [`add-source`](#deprecated-add-source)
- [`validate-source`](#validate-source)
- [`view-source`](#view-source)
- [`view-changeset`](#view-changeset)
- [`delete-changeset`](#delete-changeset)
- [`list-sources`](#list-sources)
- [`delete-source`](#delete-source)
- [`estimate-area`](#estimate-area)
Expand All @@ -78,6 +81,7 @@ export MAPBOX_ACCESS_TOKEN=my.token
- Tilesets
- [`create`](#create)
- [`publish`](#publish)
- [`publish-changeset`](#publish-changesets)
- [`update`](#update)
- [`delete`](#delete)
- [`status`](#status)
Expand Down Expand Up @@ -143,6 +147,23 @@ tilesets upload-raster-source <username> <source_id> ./file.tif
# multiple files
tilesets upload-raster-source <username> <source_id> file-1.tif file-4.tif
```
### upload-changeset

```shell
tilesets upload-changeset <username> <source_id> <file>
```

Uploads GeoJSON files to a changeset for tiling. Accepts line-delimited GeoJSON or GeoJSON feature collections as files or via `stdin`. The CLI automatically converts data to line-delimited GeoJSON prior to uploading. Can be used to add data to a source or to replace all of the data in a source with the `--replace` flag.

Please note that if your source data is a FeatureCollection, `tilesets` must read it all into memory to split it up into separate features before uploading it to the Tilesets API. You are strongly encouraged to provide your data in line-delimited GeoJSON format instead, especially if it is large.

Note: for large file uploads that are taking a very long time, try using the `--no-validation` flag.

Flags:

- `--no-validation` [optional]: do not validate source data locally before uploading, can be helpful for large file uploads
- `--replace` [optional]: delete all existing source data and replace with data from the file
- `--quiet` [optional]: do not display an upload progress bar

### _deprecated_ add-source

Expand Down Expand Up @@ -195,6 +216,33 @@ tilesets view-source <username> <source_id>

Get information for a tileset source, such as number of files, the size in bytes, and the ID in mapbox:// protocol format.

### view-changeset

```
tilesets view-changeset <username> <changeset_id>
```

Get information for a changeset, such as number of files, the size in bytes, and the ID in mapbox:// protocol format.

### delete-changeset

```
tilesets delete-changeset <username> <changeset_id>
```

Permanently delete a changeset and all of its files. This is not a recoverable action!

Flags:

- `-f` or `--force`: Do not ask for confirmation before deleting

Usage

```shell
# to delete mapbox://tileset-changeset/user/source_id
tilesets delete-changeset user source_id
```

### list-sources

```
Expand Down Expand Up @@ -451,3 +499,12 @@ Flags:
- `--limit [1-500]` [optional]: The maximum number of results to return (default: 100)
- `--indent` [optional]: Indent size for JSON output.
- `--start` [optional]: Pagination key from the `next` value in a response that has more results than the limit.


### publish-changesets

Publishes changesets for an incrementally updatable tileset. This command only supports tilesets created with the [Mapbox Tiling Service](https://docs.mapbox.com/mapbox-tiling-service/overview/).

```shell
tilesets publish-changesets <tileset_id> --changeset /path/to/changeset.json
```
2 changes: 1 addition & 1 deletion mapbox_tilesets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""mapbox_tilesets package"""

__version__ = "1.13.0"
__version__ = "1.14.0"
141 changes: 131 additions & 10 deletions mapbox_tilesets/scripts/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,20 +543,29 @@ def upload_source(

tilesets upload-source <username> <source_id> <path/to/source/data>
"""
return _upload_source(
ctx, username, id, features, no_validation, quiet, replace, token, indent
return _upload_file(
ctx, username, id, features, no_validation, quiet, replace, False, token, indent
)


def _upload_source(
ctx, username, id, features, no_validation, quiet, replace, token=None, indent=None
def _upload_file(
ctx,
username,
id,
features,
no_validation,
quiet,
replace,
changeset,
token=None,
indent=None,
):
api_endpoint = "changesets" if changeset else "sources"

mapbox_api = utils._get_api()
mapbox_token = utils._get_token(token)
s = utils._get_session()
url = (
f"{mapbox_api}/tilesets/v1/sources/{username}/{id}?access_token={mapbox_token}"
)
url = f"{mapbox_api}/tilesets/v1/{api_endpoint}/{username}/{id}?access_token={mapbox_token}"

method = "post"
if replace:
Expand Down Expand Up @@ -586,7 +595,7 @@ def _upload_source(
with tempfile.TemporaryFile() as file:
for index, feature in enumerate(features):
if not no_validation:
utils.validate_geojson(index, feature)
utils.validate_geojson(index, feature, changeset)

file.write(
(json.dumps(feature, separators=(",", ":")) + "\n").encode("utf-8")
Expand Down Expand Up @@ -749,8 +758,8 @@ def add_source(

tilesets add-source <username> <source_id> <path/to/source/data>
"""
return _upload_source(
ctx, username, id, features, no_validation, quiet, False, token, indent
return _upload_file(
ctx, username, id, features, no_validation, quiet, False, False, token, indent
)


Expand Down Expand Up @@ -981,3 +990,115 @@ def list_activity(
click.echo(json.dumps(result, indent=indent))
else:
raise errors.TilesetsError(r.text)


@cli.command("publish-changesets")
@click.argument("tileset_id", required=True, type=str)
@click.argument("changeset_payload", required=True, type=click.Path(exists=True))
@click.option("--token", "-t", required=False, type=str, help="Mapbox access token")
@click.option("--indent", type=int, default=None, help="Indent for JSON output")
def publish_changesets(tileset_id, changeset_payload, token=None, indent=None):
"""Publish changesets for a tileset.

tilesets publish-changesets <tileset_id> <path_to_changeset_payload>
"""
mapbox_api = utils._get_api()
mapbox_token = utils._get_token(token)
s = utils._get_session()
url = "{0}/tilesets/v1/{1}/publish-changesets?access_token={2}".format(
mapbox_api, tileset_id, mapbox_token
)
with open(changeset_payload) as changeset_payload_content:
changeset_payload_json = json.load(changeset_payload_content)

r = s.post(url, json=changeset_payload_json)
if r.status_code == 200:
response_msg = r.json()
click.echo(json.dumps(response_msg, indent=indent))
else:
raise errors.TilesetsError(r.text)


@cli.command("view-changeset")
@click.argument("username", required=True, type=str)
@click.argument("id", required=True, type=str)
@click.option("--token", "-t", required=False, type=str, help="Mapbox access token")
@click.option("--indent", type=int, default=None, help="Indent for JSON output")
def view_changeset(username, id, token=None, indent=None):
"""View a Changeset's information

tilesets view-changeset <username> <changeset_id>
"""
mapbox_api = utils._get_api()
mapbox_token = utils._get_token(token)
s = utils._get_session()
url = "{0}/tilesets/v1/changesets/{1}/{2}?access_token={3}".format(
mapbox_api, username, id, mapbox_token
)
r = s.get(url)
if r.status_code == 200:
click.echo(json.dumps(r.json(), indent=indent))
else:
raise errors.TilesetsError(r.text)


@cli.command("delete-changeset")
@click.argument("username", required=True, type=str)
@click.argument("id", required=True, type=str)
@click.option("--force", "-f", is_flag=True, help="Circumvents confirmation prompt")
@click.option("--token", "-t", required=False, type=str, help="Mapbox access token")
def delete_changeset(username, id, force, token=None):
"""Permanently delete a changeset and all of its files

tilesets delete-changeset <username> <changeset_id>
"""
if not force:
val = click.prompt(
'To confirm changeset deletion please enter the full changeset id "{0}/{1}"'.format(
username, id
),
type=str,
)
if val != f"{username}/{id}":
raise click.ClickException(
f"{val} does not match {username}/{id}. Aborted!"
)

mapbox_api = utils._get_api()
mapbox_token = utils._get_token(token)
s = utils._get_session()
url = "{0}/tilesets/v1/changesets/{1}/{2}?access_token={3}".format(
mapbox_api, username, id, mapbox_token
)
r = s.delete(url)
if r.status_code == 204:
click.echo("Changeset deleted.")
else:
raise errors.TilesetsError(r.text)


@cli.command("upload-changeset")
@click.argument("username", required=True, type=str)
@click.argument("id", required=True, callback=validate_source_id, type=str)
@cligj.features_in_arg
@click.option("--no-validation", is_flag=True, help="Bypass changeset file validation")
@click.option("--quiet", is_flag=True, help="Don't show progress bar")
@click.option(
"--replace",
is_flag=True,
help="Replace the existing changeset with the new changeset file",
)
@click.option("--token", "-t", required=False, type=str, help="Mapbox access token")
@click.option("--indent", type=int, default=None, help="Indent for JSON output")
@click.pass_context
def upload_changeset(
ctx, username, id, features, no_validation, quiet, replace, token=None, indent=None
):
"""Create a new changeset, or add data to an existing changeset.
Optionally, replace an existing changeset.

tilesets upload-changeset <username> <source_id> <path/to/changeset/data>
"""
return _upload_file(
ctx, username, id, features, no_validation, quiet, replace, True, token, indent
)
28 changes: 27 additions & 1 deletion mapbox_tilesets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,32 @@ def geojson_validate(index, feature):
)


def validate_geojson(index, feature):
def validate_geojson(index, feature, allow_delete=False):

if allow_delete:
delete_schema = {
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://example.com/root.json",
"type": "object",
"title": "GeoJSON Delete Schema",
"required": ["delete"],
"properties": {
"delete": {
"$id": "#/properties/delete",
"const": True,
"title": "The Delete Schema",
"examples": [True],
},
},
}

try:
validate(instance=feature, schema=delete_schema)
return
except:
pass

schema = {
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
Expand Down Expand Up @@ -127,6 +152,7 @@ def validate_geojson(index, feature):
},
},
}

try:
validate(instance=feature, schema=schema)
except ValidationError as e:
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/changeset-payload.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"layers": {
"trees": {
"changeset": "mapbox://tileset-changeset/test.id/test-changeset-id"
}
}
}
2 changes: 2 additions & 0 deletions tests/fixtures/invalid-changeset-geojson.ldgeojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"id": 1, "delete": true, "type": "Feature", "geometry": { "type": "Point","coordinates": [125.6, 10.1]},"properties": {"name": "Dinagat Islands"}}
{"id": 2, "delete": false}
3 changes: 3 additions & 0 deletions tests/fixtures/valid-changeset.ldgeojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"id": 1, "type": "Feature", "geometry": { "type": "Point","coordinates": [125.6, 10.1]},"properties": {"name": "Dinagat Islands"}}
{"id": 2, "type": "Feature", "geometry": { "type": "Point","coordinates": [-76.971938, 38.921387]},"properties": {"name": "ZELALEM INJERA"}}
{"id": 3, "delete": true}
Loading