1313else :
1414 from typing_extensions import override
1515
16+ import tempfile
17+
1618import requests
1719
1820from .api_v4 import request_get , request_post , REQUESTS_TIMEOUT
3032
3133
3234class UploadService :
35+ """
36+ Upload byte streams to the Upload Service.
37+ """
38+
3339 user_access_token : str
3440 session_key : str
3541
36- def __init__ (
37- self ,
38- user_access_token : str ,
39- session_key : str ,
40- ):
42+ def __init__ (self , user_access_token : str , session_key : str ):
4143 self .user_access_token = user_access_token
4244 self .session_key = session_key
4345
@@ -46,11 +48,7 @@ def fetch_offset(self) -> int:
4648 "Authorization" : f"OAuth { self .user_access_token } " ,
4749 }
4850 url = f"{ MAPILLARY_UPLOAD_ENDPOINT } /{ self .session_key } "
49- resp = request_get (
50- url ,
51- headers = headers ,
52- timeout = REQUESTS_TIMEOUT ,
53- )
51+ resp = request_get (url , headers = headers , timeout = REQUESTS_TIMEOUT )
5452 resp .raise_for_status ()
5553 data = resp .json ()
5654 return data ["offset" ]
@@ -59,18 +57,53 @@ def fetch_offset(self) -> int:
5957 def chunkize_byte_stream (
6058 cls , stream : T .IO [bytes ], chunk_size : int
6159 ) -> T .Generator [bytes , None , None ]:
60+ """
61+ Chunkize a byte stream into chunks of the specified size.
62+
63+ >>> list(UploadService.chunkize_byte_stream(io.BytesIO(b"foo"), 1))
64+ [b'f', b'o', b'o']
65+
66+ >>> list(UploadService.chunkize_byte_stream(io.BytesIO(b"foo"), 10))
67+ [b'foo']
68+ """
69+
6270 if chunk_size <= 0 :
6371 raise ValueError ("Expect positive chunk size" )
72+
6473 while True :
6574 data = stream .read (chunk_size )
6675 if not data :
6776 break
6877 yield data
6978
79+ @classmethod
7080 def shift_chunks (
71- self , chunks : T .Iterable [bytes ], offset : int
81+ cls , chunks : T .Iterable [bytes ], offset : int
7282 ) -> T .Generator [bytes , None , None ]:
73- assert offset >= 0 , f"Expect non-negative offset but got { offset } "
83+ """
84+ Shift the chunks by the offset.
85+
86+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 0))
87+ [b'foo', b'bar']
88+
89+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 1))
90+ [b'oo', b'bar']
91+
92+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 3))
93+ [b'bar']
94+
95+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 6))
96+ []
97+
98+ >>> list(UploadService.shift_chunks([b"foo", b"bar"], 7))
99+ []
100+
101+ >>> list(UploadService.shift_chunks([], 0))
102+ []
103+ """
104+
105+ if offset < 0 :
106+ raise ValueError (f"Expect non-negative offset but got { offset } " )
74107
75108 for chunk in chunks :
76109 if offset :
@@ -103,12 +136,10 @@ def upload_chunks(
103136 return self .upload_shifted_chunks (shifted_chunks , offset )
104137
105138 def upload_shifted_chunks (
106- self ,
107- shifted_chunks : T .Iterable [bytes ],
108- offset : int ,
139+ self , shifted_chunks : T .Iterable [bytes ], offset : int
109140 ) -> str :
110141 """
111- Upload the chunks that must already be shifted by the offset (e.g. fp.seek(begin_offset , io.SEEK_SET))
142+ Upload the chunks that must already be shifted by the offset (e.g. fp.seek(offset , io.SEEK_SET))
112143 """
113144
114145 headers = {
@@ -118,10 +149,7 @@ def upload_shifted_chunks(
118149 }
119150 url = f"{ MAPILLARY_UPLOAD_ENDPOINT } /{ self .session_key } "
120151 resp = request_post (
121- url ,
122- headers = headers ,
123- data = shifted_chunks ,
124- timeout = UPLOAD_REQUESTS_TIMEOUT ,
152+ url , headers = headers , data = shifted_chunks , timeout = UPLOAD_REQUESTS_TIMEOUT
125153 )
126154
127155 resp .raise_for_status ()
@@ -137,18 +165,35 @@ def upload_shifted_chunks(
137165
138166# A mock class for testing only
139167class FakeUploadService (UploadService ):
140- def __init__ (self , * args , ** kwargs ):
168+ """
169+ A mock upload service that simulates the upload process for testing purposes.
170+ It writes the uploaded data to a file in a temporary directory and generates a fake file handle.
171+ """
172+
173+ FILE_HANDLE_DIR : str = "file_handles"
174+
175+ def __init__ (
176+ self ,
177+ upload_path : Path | None = None ,
178+ transient_error_ratio : float = 0.0 ,
179+ * args ,
180+ ** kwargs ,
181+ ):
141182 super ().__init__ (* args , ** kwargs )
142- self ._upload_path = Path (
143- os .getenv ("MAPILLARY_UPLOAD_PATH" , "mapillary_public_uploads" )
144- )
145- self ._error_ratio = 0.02
183+ if upload_path is None :
184+ upload_path = Path (tempfile .gettempdir ()).joinpath (
185+ "mapillary_public_uploads"
186+ )
187+ self ._upload_path = upload_path
188+ self ._transient_error_ratio = transient_error_ratio
189+
190+ @property
191+ def upload_path (self ) -> Path :
192+ return self ._upload_path
146193
147194 @override
148195 def upload_shifted_chunks (
149- self ,
150- shifted_chunks : T .Iterable [bytes ],
151- offset : int ,
196+ self , shifted_chunks : T .Iterable [bytes ], offset : int
152197 ) -> str :
153198 expected_offset = self .fetch_offset ()
154199 if offset != expected_offset :
@@ -160,17 +205,17 @@ def upload_shifted_chunks(
160205 filename = self ._upload_path .joinpath (self .session_key )
161206 with filename .open ("ab" ) as fp :
162207 for chunk in shifted_chunks :
163- if random .random () <= self ._error_ratio :
208+ if random .random () <= self ._transient_error_ratio :
164209 raise requests .ConnectionError (
165- f"TEST ONLY: Failed to upload with error ratio { self ._error_ratio } "
210+ f"TEST ONLY: Failed to upload with error ratio { self ._transient_error_ratio } "
166211 )
167212 fp .write (chunk )
168- if random .random () <= self ._error_ratio :
213+ if random .random () <= self ._transient_error_ratio :
169214 raise requests .ConnectionError (
170- f"TEST ONLY: Partially uploaded with error ratio { self ._error_ratio } "
215+ f"TEST ONLY: Partially uploaded with error ratio { self ._transient_error_ratio } "
171216 )
172217
173- file_handle_dir = self ._upload_path .joinpath ("file_handles" )
218+ file_handle_dir = self ._upload_path .joinpath (self . FILE_HANDLE_DIR )
174219 file_handle_path = file_handle_dir .joinpath (self .session_key )
175220 if not file_handle_path .exists ():
176221 os .makedirs (file_handle_dir , exist_ok = True )
@@ -181,12 +226,12 @@ def upload_shifted_chunks(
181226
182227 @override
183228 def fetch_offset (self ) -> int :
184- if random .random () <= self ._error_ratio :
229+ if random .random () <= self ._transient_error_ratio :
185230 raise requests .ConnectionError (
186- f"TEST ONLY: Partially uploaded with error ratio { self ._error_ratio } "
231+ f"TEST ONLY: Partially uploaded with error ratio { self ._transient_error_ratio } "
187232 )
188- filename = os . path . join ( self ._upload_path , self .session_key )
189- if not os . path . exists (filename ):
233+ filename = self ._upload_path . joinpath ( self .session_key )
234+ if not filename . exists ():
190235 return 0
191236 with open (filename , "rb" ) as fp :
192237 fp .seek (0 , io .SEEK_END )
0 commit comments