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
1,154 changes: 68 additions & 1,086 deletions mapbox_tilesets/scripts/cli.py

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions mapbox_tilesets/scripts/cli_activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Activity-related CLI commands."""

import json
import re
from urllib.parse import parse_qs, urlencode, urlparse

import click

from mapbox_tilesets import errors, utils


@click.command("list-activity")
@click.argument("username", required=True, type=str)
@click.option(
"--sortby",
required=False,
type=click.Choice(["requests", "modified"]),
default="requests",
help="Sort the results by request count or modified timestamps (default: 'requests')",
)
@click.option(
"--orderby",
required=False,
type=click.Choice(["asc", "desc"]),
default="desc",
help="Order results by asc or desc for the sort key (default: 'desc')",
)
@click.option(
"--limit",
required=False,
type=click.IntRange(1, 500),
default=100,
help="The maximum number of results to return, from 1 to 500 (default: 100)",
)
@click.option(
"--start",
required=False,
type=str,
help="Pagination key from the `next` value in a response that has more results than the limit.",
)
@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 list_activity(
username,
sortby=None,
orderby=None,
limit=None,
start=None,
token=None,
indent=None,
):
"""List tileset activity for an account. The response is an ordered array of data about the user's tilesets and their
total requests over the past 30 days. The sorting and ordering can be configured through cli arguments, defaulting to
descending request counts.

tilesets list-activity <username>
"""
mapbox_api = utils._get_api()
mapbox_token = utils._get_token(token)
s = utils._get_session()

params = {
"access_token": mapbox_token,
"sortby": sortby,
"orderby": orderby,
"limit": limit,
"start": start,
}
params = {k: v for k, v in params.items() if v}
query_string = urlencode(params)
url = f"{mapbox_api}/activity/v1/{username}/tilesets?{query_string}"

r = s.get(url)
if r.status_code == 200:
if r.headers.get("Link"):
url = re.findall(r"<(.*)>;", r.headers.get("Link"))[0]
query = urlparse(url).query
start = parse_qs(query)["start"][0]

result = {
"data": r.json(),
"next": start,
}
click.echo(json.dumps(result, indent=indent))
else:
raise errors.TilesetsError(r.text)
121 changes: 121 additions & 0 deletions mapbox_tilesets/scripts/cli_changesets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Changeset-related CLI commands."""

import json

import click
import cligj

from mapbox_tilesets import errors, utils
from mapbox_tilesets.scripts.cli_common import _upload_file, validate_source_id


@click.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)


@click.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)


@click.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)


@click.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
)
116 changes: 116 additions & 0 deletions mapbox_tilesets/scripts/cli_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Shared CLI helpers."""

import base64
import json
import re
import tempfile

import click
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor

from mapbox_tilesets import errors, utils


def validate_source_id(ctx, param, value):
if re.match("^[a-zA-Z0-9-_]{1,32}$", value):
return value
raise click.BadParameter(
'Tileset Source ID is invalid. Must be no more than 32 characters and only include "-", "_", and alphanumeric characters.'
)


def validate_stream(features):
for index, feature in enumerate(features):
utils.validate_geojson(index, feature)
yield feature


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/{api_endpoint}/{username}/{id}?access_token={mapbox_token}"

method = "post"
if replace:
method = "put"

# This does the decoding by hand instead of using pyjwt because
# pyjwt rejects tokens that don't pad the base64 with = signs.
token_parts = mapbox_token.split(".")
if len(token_parts) < 2:
raise errors.TilesetsError(
f"Token {mapbox_token} does not contain a payload component"
)
else:
while len(token_parts[1]) % 4 != 0:
token_parts[1] = token_parts[1] + "="
body = json.loads(base64.b64decode(token_parts[1]))
if "u" in body:
if username != body["u"]:
raise errors.TilesetsError(
f"Token username {body['u']} does not match username {username}"
)
else:
raise errors.TilesetsError(
f"Token {mapbox_token} does not contain a username"
)

with tempfile.TemporaryFile() as file:
for index, feature in enumerate(features):
if not no_validation:
utils.validate_geojson(index, feature, changeset)

file.write(
(json.dumps(feature, separators=(",", ":")) + "\n").encode("utf-8")
)

file.seek(0)
m = MultipartEncoder(fields={"file": ("file", file)})

if quiet:
resp = getattr(s, method)(
url,
data=m,
headers={
"Content-Disposition": "multipart/form-data",
"Content-type": m.content_type,
},
)
else:
prog = click.progressbar(
length=m.len, fill_char="=", width=0, label="upload progress"
)
with prog:

def callback(m):
prog.pos = m.bytes_read
prog.update(0) # Step is 0 because we set pos above

monitor = MultipartEncoderMonitor(m, callback)
resp = getattr(s, method)(
url,
data=monitor,
headers={
"Content-Disposition": "multipart/form-data",
"Content-type": monitor.content_type,
},
)

if resp.status_code == 200:
click.echo(json.dumps(resp.json(), indent=indent))
else:
raise errors.TilesetsError(resp.text)
61 changes: 61 additions & 0 deletions mapbox_tilesets/scripts/cli_jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Job-related CLI commands."""

import json

import click

from mapbox_tilesets import utils


@click.command("jobs")
@click.argument("tileset", required=True, type=str)
@click.option("--stage", "-s", required=False, type=str, help="job stage")
@click.option(
"--limit",
required=False,
type=click.IntRange(1, 500),
default=100,
help="The maximum number of results to return, from 1 to 500 (default 100)",
)
@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 jobs(tileset, stage=None, limit=None, token=None, indent=None):
"""View all jobs for a particular tileset.

Only supports tilesets created with the Mapbox Tiling Service.

tilesets jobs <tileset_id>
"""
mapbox_api = utils._get_api()
mapbox_token = utils._get_token(token)
s = utils._get_session()
url = "{0}/tilesets/v1/{1}/jobs?access_token={2}".format(
mapbox_api, tileset, mapbox_token
)
url = "{0}&limit={1}".format(url, limit) if limit else url
url = "{0}&stage={1}".format(url, stage) if stage else url
r = s.get(url)
click.echo(json.dumps(r.json(), indent=indent))


@click.command("job")
@click.argument("tileset", required=True, type=str)
@click.argument("job_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 job(tileset, job_id, token=None, indent=None):
"""View a single job for a particular tileset.

Only supports tilesets created with the Mapbox Tiling Service.

tilesets job <tileset_id> <job_id>
"""
mapbox_api = utils._get_api()
mapbox_token = utils._get_token(token)
s = utils._get_session()
url = "{0}/tilesets/v1/{1}/jobs/{2}?access_token={3}".format(
mapbox_api, tileset, job_id, mapbox_token
)
r = s.get(url)

click.echo(json.dumps(r.json(), indent=indent))
Loading