Skip to content
Merged
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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ mapillary_tools process_and_upload MY_CAPTURE_DIR MY_VIDEO_DIR/*.mp4 \
--organization_key "my_organization_id"
```

To upload to an organization by a name you saved for it, use `--organization_name` instead of `--organization_key`
(see [Upload](#upload)).

The `process_and_upload` command will run the [`process`](#process) and the [`upload`](#upload) commands consecutively with combined required and optional arguments.
The command above is equivalent to:

Expand Down Expand Up @@ -262,6 +265,29 @@ mapillary_tools upload MY_CAPTURE_DIR \
--organization_key "my_organization_id"
```

To upload to an organization by a name instead of its ID, save a name for it once, with its ID.
The name is saved in the user profile in the config file:

```sh
# Upload to the organization, and save its ID under the name "work" in the profile "my_username"
mapillary_tools upload MY_CAPTURE_DIR \
--desc_path /tmp/mapillary_image_description.json \
--user_name "my_username" \
--organization_key "my_organization_id" \
--organization_name "work"

# From then on, upload to the organization by its name
mapillary_tools upload MY_CAPTURE_DIR \
--desc_path /tmp/mapillary_image_description.json \
--user_name "my_username" \
--organization_name "work"
```

`--organization_name` works with `upload`, `process_and_upload` and `video_process_and_upload`.
A name is 2 to 32 letters, numbers, hyphens and underscores, starting with a letter, and it is case-sensitive.
Saving a name that is already saved with another ID replaces the ID.
To list or delete saved names, see [Authenticate](#authenticate).

## Check Upload History

To check whether processed image sequences or videos were previously uploaded
Expand Down Expand Up @@ -524,6 +550,15 @@ Authenticate for user `my_username`. If the user is already authenticated, it wi
mapillary_tools authenticate --user_name "my_username"
```

The command lists the existing profiles first, with the organization names saved in each of them
(see [Upload](#upload) to save one). Updating the credentials keeps the saved names.

Delete the organization name `work` saved in the profile `my_username`. The profile and its other saved names are kept:

```sh
mapillary_tools authenticate --user_name "my_username" --organization_name "work" --delete
```

## Image Description

The output of the [`process`](#process) command is a JSON array of objects that describes metadata for each image or video.
Expand Down
188 changes: 177 additions & 11 deletions mapillary_tools/authenticate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@


LOG = logging.getLogger(__name__)
# Profile names and organization names
_NAME_PATTERN = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*")
_ORGANIZATION_ID_PATTERN = re.compile(r"[0-9]+")


def authenticate(
Expand All @@ -26,11 +29,18 @@ def authenticate(
user_password: str | None = None,
jwt: str | None = None,
delete: bool = False,
organization_name: str | None = None,
):
"""
Prompt for authentication information and save it to the config file
"""

if organization_name is not None and not delete:
raise exceptions.MapillaryBadParameterError(
"authenticate takes --organization_name only with --delete. To save an "
"organization name, upload with --organization_key ID --organization_name NAME"
)

# We still have to accept --user_name for the back compatibility
profile_name = user_name

Expand All @@ -54,7 +64,9 @@ def authenticate(

assert profile_name is not None, "profile_name should be set"

if delete:
if delete and organization_name is not None:
_delete_organization(profile_name, organization_name.strip(), all_user_items)
elif delete:
config.remove_config(profile_name)
LOG.info('Profile "%s" deleted', profile_name)
else:
Expand Down Expand Up @@ -90,16 +102,42 @@ def authenticate(


def fetch_user_items(
user_name: str | None = None, organization_key: str | None = None
user_name: str | None = None,
organization_key: str | None = None,
organization_name: str | None = None,
) -> config.UserItem:
"""
Read user information from the config file,
or prompt the user to authenticate if the specified profile does not exist
or prompt the user to authenticate if the specified profile does not exist.

Upload to the organization ID organization_key, and save it in the profile
under organization_name if both are given. With organization_name alone,
upload to the organization ID saved under it
"""

# we still have to accept --user_name for the back compatibility
profile_name = user_name

if organization_name is not None:
organization_name = organization_name.strip()
_validate_organization_name(organization_name)

if organization_key is not None:
organization_key = organization_key.strip()
if not _ORGANIZATION_ID_PATTERN.fullmatch(organization_key):
all_user_items = config.list_all_users()
if profile_name in all_user_items:
_list_organizations(
profile_name,
config.list_organizations(all_user_items[profile_name]),
)
elif all_user_items:
_list_all_profiles(all_user_items)
raise exceptions.MapillaryBadParameterError(
"--organization_key takes an organization ID; "
"for a saved name use --organization_name NAME"
)

all_user_items = config.list_all_users()
if not all_user_items:
authenticate(user_name=profile_name)
Expand Down Expand Up @@ -131,6 +169,17 @@ def fetch_user_items(

assert profile_name is not None, "profile_name should be set"

saved_organizations = config.list_organizations(user_items)
organization_id = organization_key
if organization_name is not None and organization_key is None:
organization_id = saved_organizations.get(organization_name)
if organization_id is None:
_list_organizations(profile_name, saved_organizations)
raise exceptions.MapillaryBadParameterError(
f'Organization "{organization_name}" not found in profile "{profile_name}". '
f"To save it, give its ID with --organization_key ID --organization_name {organization_name}"
)

try:
LOG.info(f'Verifying profile "{profile_name}"...')
user_items = _verify_user_auth(_validate_profile(user_items))
Expand All @@ -139,15 +188,31 @@ def fetch_user_items(
f'Uploading to profile "{profile_name}": {user_items.get("MAPSettingsUsername")} (ID: {user_items.get("MAPSettingsUserKey")})'
)

if organization_key is not None:
if organization_id is not None:
with api_v4.create_user_session(user_items["user_upload_token"]) as session:
resp = api_v4.fetch_organization(session, organization_key)
resp = api_v4.fetch_organization(session, organization_id)
data = api_v4.jsonify_response(resp)
LOG.info(
f"Uploading to organization: {data.get('name')} (ID: {data.get('id')})"
)
user_items["MAPOrganizationKey"] = data.get("id")

if organization_key is not None:
# The ID as the server returns it, which the upload goes to
organization_id = str(data.get("id") or organization_key)
if organization_name is not None:
_save_organization(
profile_name,
organization_name,
organization_id,
data.get("name"),
saved_organizations,
)
else:
_hint_organization_name(
profile_name, organization_id, saved_organizations
)

except requests.Timeout as ex:
raise exceptions.MapillaryUploadTimeoutError(str(ex)) from ex

Expand All @@ -157,6 +222,79 @@ def fetch_user_items(
return user_items


def _save_organization(
profile_name: str,
organization_name: str,
organization_id: str,
organization_title: str | None,
saved_organizations: dict[str, str],
) -> None:
saved_id = saved_organizations.get(organization_name)
if saved_id != organization_id:
if saved_id is not None:
LOG.warning(
'The organization "%s" (ID: %s) already exists in profile "%s" and will be overridden',
organization_name,
saved_id,
profile_name,
)
config.save_organization(profile_name, organization_name, organization_id)
LOG.info(
'Saved organization "%s": %s (ID: %s) in profile "%s"',
organization_name,
organization_title,
organization_id,
profile_name,
)


def _hint_organization_name(
profile_name: str, organization_id: str, saved_organizations: dict[str, str]
) -> None:
saved_names = [
name
for name, saved_id in saved_organizations.items()
if saved_id == organization_id
]
if saved_names:
LOG.info(
'This organization is saved in profile "%s" as "%s": next time you can use --organization_name %s',
profile_name,
saved_names[0],
saved_names[0],
)
else:
LOG.info(
"To upload to this organization by a name next time, add --organization_name NAME"
)


def _delete_organization(
profile_name: str,
organization_name: str,
all_user_items: dict[str, config.UserItem],
) -> None:
if profile_name not in all_user_items:
raise exceptions.MapillaryBadParameterError(
f'Profile "{profile_name}" not found'
)
saved_id = config.list_organizations(all_user_items[profile_name]).get(
organization_name
)
if saved_id is None or not config.remove_organization(
profile_name, organization_name
):
raise exceptions.MapillaryBadParameterError(
f'Organization "{organization_name}" not found in profile "{profile_name}"'
)
LOG.info(
'Deleted organization "%s" (ID: %s) from profile "%s"',
organization_name,
saved_id,
profile_name,
)


def _echo(*args, **kwargs):
print(*args, **kwargs, file=sys.stderr)

Expand Down Expand Up @@ -203,19 +341,27 @@ def _verify_user_auth(user_items: config.UserItem) -> config.UserItem:
}


def _validate_profile_name(profile_name: str):
if not (2 <= len(profile_name) <= 32):
def _validate_name(name: str, kind: str) -> None:
if not (2 <= len(name) <= 32):
raise exceptions.MapillaryBadParameterError(
"Profile name must be between 2 and 32 characters long"
f"{kind.capitalize()} must be between 2 and 32 characters long"
)

pattern = re.compile(r"^[a-zA-Z]+[a-zA-Z0-9_-]*$")
if not bool(pattern.match(profile_name)):
if not _NAME_PATTERN.fullmatch(name):
raise exceptions.MapillaryBadParameterError(
"Invalid profile name. Use only letters, numbers, hyphens and underscores"
f'Invalid {kind} "{name}". Use only letters, numbers, hyphens and underscores, starting with a letter'
)


def _validate_profile_name(profile_name: str):
_validate_name(profile_name, "profile name")


def _validate_organization_name(organization_name: str):
# The same rules as profile names, so that a name is never taken for an ID
_validate_name(organization_name, "organization name")


def _list_all_profiles(profiles: dict[str, config.UserItem]) -> None:
_echo("Existing Mapillary profiles:")

Expand All @@ -228,6 +374,26 @@ def _list_all_profiles(profiles: dict[str, config.UserItem]) -> None:
user_id = items.get("MAPSettingsUserKey", "N/A")
username = items.get("MAPSettingsUsername", "N/A")
_echo(f"{idx:>5}. {name:<32} {user_id:>16} {username:>32}")
for organization_name, organization_id in config.list_organizations(
items
).items():
_echo(
f"{'':>7} {'organization ' + organization_name:<30} {organization_id:>16}"
)


def _list_organizations(profile_name: str, organizations: dict[str, str]) -> None:
if not organizations:
_echo(f'No organizations saved in profile "{profile_name}"')
return

_echo(f'Saved organizations in profile "{profile_name}":')

# Header
_echo(f"{'':>5} {'Organization name':<32} {'Organization ID':>16}")

for idx, (name, organization_id) in enumerate(organizations.items(), 1):
_echo(f"{idx:>5}. {name:<32} {organization_id:>16}")


def _is_interactive():
Expand Down
8 changes: 7 additions & 1 deletion mapillary_tools/commands/authenticate.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,17 @@ def add_basic_arguments(self, parser: argparse.ArgumentParser):
)
parser.add_argument(
"--delete",
help="Delete the specified user profile",
help="Delete the specified user profile, or with --organization_name, only that saved organization name",
default=False,
required=False,
action="store_true",
)
parser.add_argument(
"--organization_name",
help="The saved organization name to delete from the user profile, with --delete",
default=None,
required=False,
)

def run(self, vars_args: dict):
authenticate(
Expand Down
8 changes: 7 additions & 1 deletion mapillary_tools/commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ def add_common_upload_options(group):
)
group.add_argument(
"--organization_key",
help="The Mapillary organization ID to upload to.",
help="The Mapillary organization ID to upload to. Add --organization_name NAME to save the ID under NAME in the user profile.",
default=None,
required=False,
)
group.add_argument(
"--organization_name",
help="Upload to the organization saved under this name in the user profile. To save a name, give it with the organization ID: --organization_key ID --organization_name NAME. Names are 2 to 32 letters, numbers, hyphens and underscores, starting with a letter.",
default=None,
required=False,
)
Expand Down
Loading
Loading