mapillary-tools 0.14.3__tar.gz → 0.14.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/PKG-INFO +1 -1
- mapillary_tools-0.14.5/mapillary_tools/__init__.py +1 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/exif_write.py +61 -29
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/history.py +87 -57
- mapillary_tools-0.14.5/mapillary_tools/store.py +128 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/uploader.py +1 -1
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/utils.py +1 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools.egg-info/PKG-INFO +1 -1
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools.egg-info/SOURCES.txt +1 -0
- mapillary_tools-0.14.3/mapillary_tools/__init__.py +0 -1
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/LICENSE +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/README.md +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/api_v4.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/authenticate.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/blackvue_parser.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/camm/camm_builder.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/camm/camm_parser.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/__init__.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/__main__.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/authenticate.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/process.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/process_and_upload.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/sample_video.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/upload.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/video_process.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/video_process_and_upload.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/zip.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/config.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/constants.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/exceptions.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/exif_read.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/exiftool_read.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/exiftool_read_video.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/exiftool_runner.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/ffmpeg.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geo.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/__init__.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/base.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/factory.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_images_from_exif.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_images_from_exiftool.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_images_from_gpx.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_images_from_gpx_file.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_images_from_nmea_file.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_images_from_video.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_videos_from_exiftool.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_videos_from_gpx.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_videos_from_video.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/image_extractors/base.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/image_extractors/exif.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/image_extractors/exiftool.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/options.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/utils.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/video_extractors/base.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/video_extractors/exiftool.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/video_extractors/gpx.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/video_extractors/native.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/gpmf/gpmf_gps_filter.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/gpmf/gpmf_parser.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/gpmf/gps_filter.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/http.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/ipc.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/mp4/__init__.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/mp4/construct_mp4_parser.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/mp4/io_utils.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/mp4/mp4_sample_parser.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/mp4/simple_mp4_builder.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/mp4/simple_mp4_parser.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/process_geotag_properties.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/process_sequence_properties.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/sample_video.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/serializer/description.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/serializer/gpx.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/telemetry.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/types.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/upload.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/upload_api_v4.py +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools.egg-info/dependency_links.txt +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools.egg-info/entry_points.txt +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools.egg-info/requires.txt +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools.egg-info/top_level.txt +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/pyproject.toml +0 -0
- {mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "0.14.5"
|
|
@@ -6,6 +6,7 @@ import io
|
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
8
|
import math
|
|
9
|
+
from fractions import Fraction
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
import piexif
|
|
@@ -29,16 +30,19 @@ class ExifEdit:
|
|
|
29
30
|
|
|
30
31
|
@staticmethod
|
|
31
32
|
def decimal_to_dms(
|
|
32
|
-
value: float,
|
|
33
|
-
) -> tuple[tuple[
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
value: float,
|
|
34
|
+
) -> tuple[tuple[int, int], tuple[int, int], tuple[int, int]]:
|
|
35
|
+
"""Convert decimal position to Exif degrees, minutes, and seconds rationals"""
|
|
36
|
+
|
|
37
|
+
deg: int = int(value)
|
|
38
|
+
min: int = int(value := (value - deg) * 60)
|
|
39
|
+
sec: float = (value - min) * 60
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
(deg, 1),
|
|
43
|
+
(min, 1),
|
|
44
|
+
(Fraction.from_float(sec).limit_denominator().as_integer_ratio()),
|
|
45
|
+
)
|
|
42
46
|
|
|
43
47
|
def add_image_description(self, data: dict) -> None:
|
|
44
48
|
"""Add a dict to image description."""
|
|
@@ -83,41 +87,69 @@ class ExifEdit:
|
|
|
83
87
|
self._ef["GPS"][piexif.GPSIFD.GPSTimeStamp] = (
|
|
84
88
|
(dt.hour, 1),
|
|
85
89
|
(dt.minute, 1),
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
(
|
|
91
|
+
Fraction.from_float(dt.second + dt.microsecond / 1e6)
|
|
92
|
+
.limit_denominator()
|
|
93
|
+
.as_integer_ratio()
|
|
94
|
+
),
|
|
88
95
|
)
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
if LOG.isEnabledFor(logging.DEBUG):
|
|
97
|
+
LOG.debug(
|
|
98
|
+
'GPSDateStamp: "%s"\tGPSTimeStamp: %s',
|
|
99
|
+
self._ef["GPS"][piexif.GPSIFD.GPSDateStamp],
|
|
100
|
+
self._ef["GPS"][piexif.GPSIFD.GPSTimeStamp],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def add_lat_lon(self, lat: float, lon: float) -> None:
|
|
91
104
|
"""Add lat, lon to gps (lat, lon in float)."""
|
|
105
|
+
|
|
92
106
|
self._ef["GPS"][piexif.GPSIFD.GPSLatitudeRef] = "N" if lat > 0 else "S"
|
|
107
|
+
self._ef["GPS"][piexif.GPSIFD.GPSLatitude] = ExifEdit.decimal_to_dms(
|
|
108
|
+
math.fabs(lat)
|
|
109
|
+
)
|
|
93
110
|
self._ef["GPS"][piexif.GPSIFD.GPSLongitudeRef] = "E" if lon > 0 else "W"
|
|
94
111
|
self._ef["GPS"][piexif.GPSIFD.GPSLongitude] = ExifEdit.decimal_to_dms(
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
self._ef["GPS"][piexif.GPSIFD.GPSLatitude] = ExifEdit.decimal_to_dms(
|
|
98
|
-
abs(lat), int(precision)
|
|
112
|
+
math.fabs(lon)
|
|
99
113
|
)
|
|
114
|
+
if LOG.isEnabledFor(logging.DEBUG):
|
|
115
|
+
LOG.debug(
|
|
116
|
+
"GPSLatitude: %s\tGPSLongitude: %s",
|
|
117
|
+
self._ef["GPS"][piexif.GPSIFD.GPSLatitude],
|
|
118
|
+
self._ef["GPS"][piexif.GPSIFD.GPSLongitude],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def add_altitude(self, altitude: float) -> None:
|
|
122
|
+
"""Add altitude."""
|
|
100
123
|
|
|
101
|
-
def add_altitude(self, altitude: float, precision: int = 100) -> None:
|
|
102
|
-
"""Add altitude (pre is the precision)."""
|
|
103
124
|
ref = 0 if altitude > 0 else 1
|
|
104
125
|
self._ef["GPS"][piexif.GPSIFD.GPSAltitude] = (
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
Fraction.from_float(math.fabs(altitude))
|
|
127
|
+
.limit_denominator()
|
|
128
|
+
.as_integer_ratio()
|
|
107
129
|
)
|
|
108
130
|
self._ef["GPS"][piexif.GPSIFD.GPSAltitudeRef] = ref
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
if LOG.isEnabledFor(logging.DEBUG):
|
|
132
|
+
LOG.debug(
|
|
133
|
+
'GPSAltitudeRef: "%s"\tGPSAltitude: %s',
|
|
134
|
+
self._ef["GPS"][piexif.GPSIFD.GPSAltitudeRef],
|
|
135
|
+
self._ef["GPS"][piexif.GPSIFD.GPSAltitude],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def add_direction(self, direction: float, ref: str = "T") -> None:
|
|
113
139
|
"""Add image direction."""
|
|
140
|
+
|
|
114
141
|
# normalize direction
|
|
115
|
-
direction = direction
|
|
142
|
+
direction = math.fmod(direction, 360.0)
|
|
116
143
|
self._ef["GPS"][piexif.GPSIFD.GPSImgDirection] = (
|
|
117
|
-
|
|
118
|
-
precision,
|
|
144
|
+
Fraction.from_float(direction).limit_denominator().as_integer_ratio()
|
|
119
145
|
)
|
|
120
146
|
self._ef["GPS"][piexif.GPSIFD.GPSImgDirectionRef] = ref
|
|
147
|
+
if LOG.isEnabledFor(logging.DEBUG):
|
|
148
|
+
LOG.debug(
|
|
149
|
+
'GPSImgDirectionRef: "%s"\tGPSImgDirection: %s',
|
|
150
|
+
self._ef["GPS"][piexif.GPSIFD.GPSImgDirectionRef],
|
|
151
|
+
self._ef["GPS"][piexif.GPSIFD.GPSImgDirection],
|
|
152
|
+
)
|
|
121
153
|
|
|
122
154
|
def add_make(self, make: str) -> None:
|
|
123
155
|
if not make:
|
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import contextlib
|
|
4
|
-
import dbm
|
|
5
3
|
import json
|
|
6
4
|
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sqlite3
|
|
7
7
|
import string
|
|
8
8
|
import threading
|
|
9
9
|
import time
|
|
10
10
|
import typing as T
|
|
11
|
+
from functools import wraps
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
# Otherwise you will see: ImportError: no dbm clone found; tried ['dbm.sqlite3', 'dbm.gnu', 'dbm.ndbm', 'dbm.dumb']
|
|
15
|
-
try:
|
|
16
|
-
import dbm.sqlite3 # type: ignore
|
|
17
|
-
except ImportError:
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
from . import constants, types
|
|
14
|
+
from . import constants, store, types
|
|
22
15
|
from .serializer.description import DescriptionJSONSerializer
|
|
23
16
|
|
|
24
17
|
JSONDict = T.Dict[str, T.Union[str, int, float, None]]
|
|
@@ -85,103 +78,140 @@ def write_history(
|
|
|
85
78
|
fp.write(json.dumps(history))
|
|
86
79
|
|
|
87
80
|
|
|
81
|
+
def _retry_on_database_lock_error(fn):
|
|
82
|
+
"""
|
|
83
|
+
Decorator to retry a function if it raises a sqlite3.OperationalError with
|
|
84
|
+
"database is locked" in the message.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
@wraps(fn)
|
|
88
|
+
def wrapper(*args, **kwargs):
|
|
89
|
+
while True:
|
|
90
|
+
try:
|
|
91
|
+
return fn(*args, **kwargs)
|
|
92
|
+
except sqlite3.OperationalError as ex:
|
|
93
|
+
if "database is locked" in str(ex).lower():
|
|
94
|
+
LOG.warning(f"{str(ex)}")
|
|
95
|
+
LOG.info("Retrying in 1 second...")
|
|
96
|
+
time.sleep(1)
|
|
97
|
+
else:
|
|
98
|
+
raise ex
|
|
99
|
+
|
|
100
|
+
return wrapper
|
|
101
|
+
|
|
102
|
+
|
|
88
103
|
class PersistentCache:
|
|
89
|
-
_lock:
|
|
104
|
+
_lock: threading.Lock
|
|
90
105
|
|
|
91
106
|
def __init__(self, file: str):
|
|
92
|
-
# SQLite3 backend supports concurrent access without a lock
|
|
93
|
-
if dbm.whichdb(file) == "dbm.sqlite3":
|
|
94
|
-
self._lock = contextlib.nullcontext()
|
|
95
|
-
else:
|
|
96
|
-
self._lock = threading.Lock()
|
|
97
107
|
self._file = file
|
|
108
|
+
self._lock = threading.Lock()
|
|
98
109
|
|
|
99
110
|
def get(self, key: str) -> str | None:
|
|
111
|
+
if not self._db_existed():
|
|
112
|
+
return None
|
|
113
|
+
|
|
100
114
|
s = time.perf_counter()
|
|
101
115
|
|
|
102
|
-
with self.
|
|
103
|
-
|
|
104
|
-
|
|
116
|
+
with store.KeyValueStore(self._file, flag="r") as db:
|
|
117
|
+
try:
|
|
118
|
+
raw_payload: bytes | None = db.get(key) # data retrieved from db[key]
|
|
119
|
+
except Exception as ex:
|
|
120
|
+
if self._table_not_found(ex):
|
|
121
|
+
return None
|
|
122
|
+
raise ex
|
|
105
123
|
|
|
106
|
-
if
|
|
124
|
+
if raw_payload is None:
|
|
107
125
|
return None
|
|
108
126
|
|
|
109
|
-
|
|
127
|
+
data: JSONDict = self._decode(raw_payload) # JSON dict decoded from db[key]
|
|
110
128
|
|
|
111
|
-
if self._is_expired(
|
|
129
|
+
if self._is_expired(data):
|
|
112
130
|
return None
|
|
113
131
|
|
|
114
|
-
|
|
132
|
+
cached_value = data.get("value") # value in the JSON dict decoded from db[key]
|
|
115
133
|
|
|
116
134
|
LOG.debug(
|
|
117
135
|
f"Found file handle for {key} in cache ({(time.perf_counter() - s) * 1000:.0f} ms)"
|
|
118
136
|
)
|
|
119
137
|
|
|
120
|
-
return T.cast(str,
|
|
138
|
+
return T.cast(str, cached_value)
|
|
121
139
|
|
|
122
|
-
|
|
140
|
+
@_retry_on_database_lock_error
|
|
141
|
+
def set(self, key: str, value: str, expires_in: int = 3600 * 24 * 2) -> None:
|
|
123
142
|
s = time.perf_counter()
|
|
124
143
|
|
|
125
|
-
|
|
144
|
+
data = {
|
|
126
145
|
"expires_at": time.time() + expires_in,
|
|
127
|
-
"
|
|
146
|
+
"value": value,
|
|
128
147
|
}
|
|
129
148
|
|
|
130
|
-
|
|
149
|
+
payload: bytes = json.dumps(data).encode("utf-8")
|
|
131
150
|
|
|
132
151
|
with self._lock:
|
|
133
|
-
with
|
|
134
|
-
db[key] =
|
|
152
|
+
with store.KeyValueStore(self._file, flag="c") as db:
|
|
153
|
+
db[key] = payload
|
|
135
154
|
|
|
136
155
|
LOG.debug(
|
|
137
156
|
f"Cached file handle for {key} ({(time.perf_counter() - s) * 1000:.0f} ms)"
|
|
138
157
|
)
|
|
139
158
|
|
|
159
|
+
@_retry_on_database_lock_error
|
|
140
160
|
def clear_expired(self) -> list[str]:
|
|
141
|
-
s = time.perf_counter()
|
|
142
|
-
|
|
143
161
|
expired_keys: list[str] = []
|
|
144
162
|
|
|
145
|
-
|
|
146
|
-
with dbm.open(self._file, flag="c") as db:
|
|
147
|
-
if hasattr(db, "items"):
|
|
148
|
-
items: T.Iterable[tuple[str | bytes, bytes]] = db.items()
|
|
149
|
-
else:
|
|
150
|
-
items = ((key, db[key]) for key in db.keys())
|
|
163
|
+
s = time.perf_counter()
|
|
151
164
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
with self._lock:
|
|
166
|
+
with store.KeyValueStore(self._file, flag="c") as db:
|
|
167
|
+
for key, raw_payload in db.items():
|
|
168
|
+
data = self._decode(raw_payload)
|
|
169
|
+
if self._is_expired(data):
|
|
155
170
|
del db[key]
|
|
156
171
|
expired_keys.append(T.cast(str, key))
|
|
157
172
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
173
|
+
LOG.debug(
|
|
174
|
+
f"Cleared {len(expired_keys)} expired entries from the cache ({(time.perf_counter() - s) * 1000:.0f} ms)"
|
|
175
|
+
)
|
|
162
176
|
|
|
163
177
|
return expired_keys
|
|
164
178
|
|
|
165
|
-
def keys(self):
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return db.keys()
|
|
179
|
+
def keys(self) -> list[str]:
|
|
180
|
+
if not self._db_existed():
|
|
181
|
+
return []
|
|
169
182
|
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
try:
|
|
184
|
+
with store.KeyValueStore(self._file, flag="r") as db:
|
|
185
|
+
return [key.decode("utf-8") for key in db.keys()]
|
|
186
|
+
except Exception as ex:
|
|
187
|
+
if self._table_not_found(ex):
|
|
188
|
+
return []
|
|
189
|
+
raise ex
|
|
190
|
+
|
|
191
|
+
def _is_expired(self, data: JSONDict) -> bool:
|
|
192
|
+
expires_at = data.get("expires_at")
|
|
172
193
|
if isinstance(expires_at, (int, float)):
|
|
173
194
|
return expires_at is None or expires_at <= time.time()
|
|
174
195
|
return False
|
|
175
196
|
|
|
176
|
-
def _decode(self,
|
|
197
|
+
def _decode(self, raw_payload: bytes) -> JSONDict:
|
|
177
198
|
try:
|
|
178
|
-
|
|
199
|
+
data = json.loads(raw_payload.decode("utf-8"))
|
|
179
200
|
except json.JSONDecodeError as ex:
|
|
180
201
|
LOG.warning(f"Failed to decode cache value: {ex}")
|
|
181
202
|
return {}
|
|
182
203
|
|
|
183
|
-
if not isinstance(
|
|
184
|
-
LOG.warning(f"Invalid cache value format: {
|
|
204
|
+
if not isinstance(data, dict):
|
|
205
|
+
LOG.warning(f"Invalid cache value format: {raw_payload!r}")
|
|
185
206
|
return {}
|
|
186
207
|
|
|
187
|
-
return
|
|
208
|
+
return data
|
|
209
|
+
|
|
210
|
+
def _db_existed(self) -> bool:
|
|
211
|
+
return os.path.exists(self._file)
|
|
212
|
+
|
|
213
|
+
def _table_not_found(self, ex: Exception) -> bool:
|
|
214
|
+
if isinstance(ex, sqlite3.OperationalError):
|
|
215
|
+
if "no such table" in str(ex):
|
|
216
|
+
return True
|
|
217
|
+
return False
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides a persistent key-value store based on SQLite.
|
|
3
|
+
|
|
4
|
+
This implementation is mostly copied from dbm.sqlite3 in the Python standard library,
|
|
5
|
+
but works for Python >= 3.9, whereas dbm.sqlite3 is only available for Python 3.13.
|
|
6
|
+
|
|
7
|
+
Source: https://github.com/python/cpython/blob/3.13/Lib/dbm/sqlite3.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sqlite3
|
|
12
|
+
import sys
|
|
13
|
+
from collections.abc import MutableMapping
|
|
14
|
+
from contextlib import closing, suppress
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
BUILD_TABLE = """
|
|
18
|
+
CREATE TABLE IF NOT EXISTS Dict (
|
|
19
|
+
key BLOB UNIQUE NOT NULL,
|
|
20
|
+
value BLOB NOT NULL
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
GET_SIZE = "SELECT COUNT (key) FROM Dict"
|
|
24
|
+
LOOKUP_KEY = "SELECT value FROM Dict WHERE key = CAST(? AS BLOB)"
|
|
25
|
+
STORE_KV = "REPLACE INTO Dict (key, value) VALUES (CAST(? AS BLOB), CAST(? AS BLOB))"
|
|
26
|
+
DELETE_KEY = "DELETE FROM Dict WHERE key = CAST(? AS BLOB)"
|
|
27
|
+
ITER_KEYS = "SELECT key FROM Dict"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _normalize_uri(path):
|
|
31
|
+
path = Path(path)
|
|
32
|
+
uri = path.absolute().as_uri()
|
|
33
|
+
while "//" in uri:
|
|
34
|
+
uri = uri.replace("//", "/")
|
|
35
|
+
return uri
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class KeyValueStore(MutableMapping):
|
|
39
|
+
def __init__(self, path, /, *, flag="r", mode=0o666):
|
|
40
|
+
"""Open a key-value database and return the object.
|
|
41
|
+
|
|
42
|
+
The 'path' parameter is the name of the database file.
|
|
43
|
+
|
|
44
|
+
The optional 'flag' parameter can be one of ...:
|
|
45
|
+
'r' (default): open an existing database for read only access
|
|
46
|
+
'w': open an existing database for read/write access
|
|
47
|
+
'c': create a database if it does not exist; open for read/write access
|
|
48
|
+
'n': always create a new, empty database; open for read/write access
|
|
49
|
+
|
|
50
|
+
The optional 'mode' parameter is the Unix file access mode of the database;
|
|
51
|
+
only used when creating a new database. Default: 0o666.
|
|
52
|
+
"""
|
|
53
|
+
path = os.fsdecode(path)
|
|
54
|
+
if flag == "r":
|
|
55
|
+
flag = "ro"
|
|
56
|
+
elif flag == "w":
|
|
57
|
+
flag = "rw"
|
|
58
|
+
elif flag == "c":
|
|
59
|
+
flag = "rwc"
|
|
60
|
+
Path(path).touch(mode=mode, exist_ok=True)
|
|
61
|
+
elif flag == "n":
|
|
62
|
+
flag = "rwc"
|
|
63
|
+
Path(path).unlink(missing_ok=True)
|
|
64
|
+
Path(path).touch(mode=mode)
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError(f"Flag must be one of 'r', 'w', 'c', or 'n', not {flag!r}")
|
|
67
|
+
|
|
68
|
+
# We use the URI format when opening the database.
|
|
69
|
+
uri = _normalize_uri(path)
|
|
70
|
+
uri = f"{uri}?mode={flag}"
|
|
71
|
+
|
|
72
|
+
if sys.version_info >= (3, 12):
|
|
73
|
+
# This is the preferred way, but only available in Python 3.10 and newer.
|
|
74
|
+
self._cx = sqlite3.connect(uri, autocommit=True, uri=True)
|
|
75
|
+
else:
|
|
76
|
+
self._cx = sqlite3.connect(uri, uri=True)
|
|
77
|
+
|
|
78
|
+
# This is an optimization only; it's ok if it fails.
|
|
79
|
+
with suppress(sqlite3.OperationalError):
|
|
80
|
+
self._cx.execute("PRAGMA journal_mode = wal")
|
|
81
|
+
|
|
82
|
+
if flag == "rwc":
|
|
83
|
+
self._execute(BUILD_TABLE)
|
|
84
|
+
|
|
85
|
+
def _execute(self, *args, **kwargs):
|
|
86
|
+
if sys.version_info >= (3, 12):
|
|
87
|
+
return closing(self._cx.execute(*args, **kwargs))
|
|
88
|
+
else:
|
|
89
|
+
# Use a context manager to commit the changes
|
|
90
|
+
with self._cx:
|
|
91
|
+
return closing(self._cx.execute(*args, **kwargs))
|
|
92
|
+
|
|
93
|
+
def __len__(self):
|
|
94
|
+
with self._execute(GET_SIZE) as cu:
|
|
95
|
+
row = cu.fetchone()
|
|
96
|
+
return row[0]
|
|
97
|
+
|
|
98
|
+
def __getitem__(self, key):
|
|
99
|
+
with self._execute(LOOKUP_KEY, (key,)) as cu:
|
|
100
|
+
row = cu.fetchone()
|
|
101
|
+
if not row:
|
|
102
|
+
raise KeyError(key)
|
|
103
|
+
return row[0]
|
|
104
|
+
|
|
105
|
+
def __setitem__(self, key, value):
|
|
106
|
+
self._execute(STORE_KV, (key, value))
|
|
107
|
+
|
|
108
|
+
def __delitem__(self, key):
|
|
109
|
+
with self._execute(DELETE_KEY, (key,)) as cu:
|
|
110
|
+
if not cu.rowcount:
|
|
111
|
+
raise KeyError(key)
|
|
112
|
+
|
|
113
|
+
def __iter__(self):
|
|
114
|
+
with self._execute(ITER_KEYS) as cu:
|
|
115
|
+
for row in cu:
|
|
116
|
+
yield row[0]
|
|
117
|
+
|
|
118
|
+
def close(self):
|
|
119
|
+
self._cx.close()
|
|
120
|
+
|
|
121
|
+
def keys(self):
|
|
122
|
+
return list(super().keys())
|
|
123
|
+
|
|
124
|
+
def __enter__(self):
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def __exit__(self, *args):
|
|
128
|
+
self.close()
|
|
@@ -1311,7 +1311,7 @@ def _is_uuid(key: str) -> bool:
|
|
|
1311
1311
|
|
|
1312
1312
|
|
|
1313
1313
|
def _build_upload_cache_path(upload_options: UploadOptions) -> Path:
|
|
1314
|
-
# Different python/CLI versions use different cache
|
|
1314
|
+
# Different python/CLI versions use different cache formats.
|
|
1315
1315
|
# Separate them to avoid conflicts
|
|
1316
1316
|
py_version_parts = [str(part) for part in sys.version_info[:3]]
|
|
1317
1317
|
version = f"py_{'_'.join(py_version_parts)}_{VERSION}"
|
|
@@ -21,6 +21,7 @@ mapillary_tools/ipc.py
|
|
|
21
21
|
mapillary_tools/process_geotag_properties.py
|
|
22
22
|
mapillary_tools/process_sequence_properties.py
|
|
23
23
|
mapillary_tools/sample_video.py
|
|
24
|
+
mapillary_tools/store.py
|
|
24
25
|
mapillary_tools/telemetry.py
|
|
25
26
|
mapillary_tools/types.py
|
|
26
27
|
mapillary_tools/upload.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "0.14.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/commands/process_and_upload.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_images_from_exif.py
RENAMED
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_images_from_gpx.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_images_from_video.py
RENAMED
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_videos_from_gpx.py
RENAMED
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/geotag_videos_from_video.py
RENAMED
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/image_extractors/base.py
RENAMED
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/image_extractors/exif.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/video_extractors/base.py
RENAMED
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/video_extractors/gpx.py
RENAMED
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/geotag/video_extractors/native.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/mp4/construct_mp4_parser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/process_geotag_properties.py
RENAMED
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools/process_sequence_properties.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mapillary_tools-0.14.3 → mapillary_tools-0.14.5}/mapillary_tools.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|