22
33import json
44import logging
5- import math
65import time
76import uuid
87from fractions import Fraction
3938
4039# get version
4140try :
42- __version__ = version ("bma-client-lib " )
41+ __version__ = version ("bma_client_lib " )
4342except PackageNotFoundError :
4443 __version__ = "0.0.0"
4544
@@ -79,6 +78,11 @@ def __init__(
7978 self .skip_exif_tags = SKIP_EXIF_TAGS
8079 self .get_server_settings ()
8180 self .__version__ = __version__
81+ # build client object
82+ self .clientjson = {
83+ "client_uuid" : self .uuid ,
84+ "client_version" : f"bma-client-lib { __version__ } " ,
85+ }
8286
8387 def update_access_token (self ) -> None :
8488 """Set or update self.access_token using self.refresh_token."""
@@ -104,54 +108,57 @@ def get_server_settings(self) -> dict[str, dict[str, dict[str, list[str]]]]:
104108 self .base_url + "/api/v1/json/jobs/settings/" ,
105109 ).raise_for_status ()
106110 self .settings = r .json ()["bma_response" ]
107- return r . json ()
111+ return self . settings # type: ignore[no-any-return]
108112
109113 def get_jobs (self , job_filter : str = "?limit=0" ) -> list [Job ]:
110114 """Get a filtered list of the jobs this user has access to."""
111115 r = self .client .get (self .base_url + f"/api/v1/json/jobs/{ job_filter } " ).raise_for_status ()
112116 response = r .json ()["bma_response" ]
113117 logger .debug (f"Returning { len (response )} jobs with filter { job_filter } " )
114- return response
118+ return response # type: ignore[no-any-return]
115119
116120 def get_file_info (self , file_uuid : uuid .UUID ) -> dict [str , str ]:
117121 """Get metadata for a file."""
118122 r = self .client .get (self .base_url + f"/api/v1/json/files/{ file_uuid } /" ).raise_for_status ()
119- return r .json ()["bma_response" ]
123+ return r .json ()["bma_response" ] # type: ignore[no-any-return]
120124
121125 def download (self , url : str , path : Path ) -> Path :
122126 """Download a file to a path."""
123127 r = self .client .get (url ).raise_for_status ()
124128 logger .debug (f"Done downloading { len (r .content )} bytes from { url } , saving to { path } " )
129+ path .parent .mkdir (parents = True , exist_ok = True )
125130 with path .open ("wb" ) as f :
126131 f .write (r .content )
127132 return path
128133
129134 def download_job_source (self , job : Job ) -> Path :
130135 """Download the file needed to do a job."""
136+ # skip the leading slash when using url as a local path
137+ path = self .path / job .source_url [1 :]
138+ if path .exists ():
139+ # file was downloaded previously
140+ return path
141+ # get the file
131142 return self .download (
132143 url = self .base_url + job .source_url ,
133- path = self . path / job . source_filename ,
144+ path = path ,
134145 )
135146
136147 def get_job_assignment (self , job_filter : str = "" ) -> list [Job ]:
137148 """Ask for new job(s) from the API."""
138149 url = self .base_url + "/api/v1/json/jobs/assign/"
139150 if job_filter :
140151 url += job_filter
141- data = {
142- "client_uuid" : self .uuid ,
143- "client_version" : f"bma-client-lib { __version__ } " ,
144- }
145152 try :
146- r = self .client .post (url , json = data ).raise_for_status ()
153+ r = self .client .post (url , json = self . clientjson ).raise_for_status ()
147154 response = r .json ()["bma_response" ]
148155 except httpx .HTTPStatusError as e :
149156 if e .response .status_code == HTTPStatus .NOT_FOUND :
150157 response = []
151158 else :
152159 raise
153160 logger .debug (f"Returning { len (response )} assigned jobs" )
154- return response
161+ return response # type: ignore[no-any-return]
155162
156163 def unassign_job (self , job : Job ) -> bool :
157164 """Unassign a job."""
@@ -163,7 +170,7 @@ def unassign_job(self, job: Job) -> bool:
163170
164171 def upload_file (self , path : Path , attribution : str , file_license : str ) -> dict [str , dict [str , str ]]:
165172 """Upload a file."""
166- # get mimetype
173+ # get mimetype using magic on the first 2kb of the file
167174 with path .open ("rb" ) as fh :
168175 mimetype = magic .from_buffer (fh .read (2048 ), mime = True )
169176
@@ -209,11 +216,11 @@ def upload_file(self, path: Path, attribution: str, file_license: str) -> dict[s
209216 # doit
210217 r = self .client .post (
211218 self .base_url + "/api/v1/json/files/upload/" ,
212- data = {"metadata " : json .dumps (data )},
219+ data = {"f_metadata " : json .dumps (data ), "client" : json . dumps ( self . clientjson )},
213220 files = files ,
214221 timeout = 30 ,
215222 )
216- return r .json ()
223+ return r .json () # type: ignore[no-any-return]
217224
218225 def handle_job (self , job : Job ) -> None :
219226 """Do the thing and upload the result."""
@@ -236,7 +243,7 @@ def handle_job(self, job: Job) -> None:
236243 raise JobNotSupportedError (job = job )
237244 source = self .download_job_source (job )
238245 result = self .create_thumbnail_source (job = job )
239- filename = job .source_filename
246+ filename = job .source_url
240247
241248 else :
242249 raise JobNotSupportedError (job = job )
@@ -264,6 +271,7 @@ def write_and_upload_result(self, job: Job, result: "JobResult", filename: str)
264271 kwargs ["append_images" ] = image [1 :]
265272 kwargs ["save_all" ] = True
266273 image [0 ].save (buf , format = job .filetype , exif = exif , ** kwargs )
274+ metadata = {"width" : image [0 ].width , "height" : image [0 ].height , "mimetype" : job .mimetype }
267275
268276 elif isinstance (job , ImageExifExtractionJob ):
269277 logger .debug (f"Got exif data { result } " )
@@ -329,6 +337,7 @@ def handle_image_conversion_job(
329337 logger .debug (f"Desired image size is { size } , aspect ratio: { ratio } ({ orig_str } ), converting image..." )
330338 start = time .time ()
331339 images = transform_image (original_img = image , crop_w = size [0 ], crop_h = size [1 ])
340+ logger .debug (f"Result image size is { images [0 ].width } *{ images [0 ].height } " )
332341 logger .debug (f"Converting image size and AR took { time .time () - start } seconds" )
333342
334343 logger .debug ("Done, returning result..." )
@@ -340,20 +349,15 @@ def upload_job_result(
340349 buf : "BytesIO" ,
341350 filename : str ,
342351 metadata : dict [str , str | int ] | None = None ,
343- ) -> dict :
352+ ) -> dict [ str , str ] :
344353 """Upload the result of a job."""
345354 size = buf .getbuffer ().nbytes
346355 logger .debug (f"Uploading { size } bytes result for job { job .job_uuid } with filename { filename } " )
347356 start = time .time ()
348357 files = {"f" : (filename , buf )}
349- # build client object
350- client = {
351- "client_uuid" : self .uuid ,
352- "client_version" : "bma-client-lib {__version__}" ,
353- }
354- data = {"client" : json .dumps (client )}
355- if isinstance (job , ThumbnailSourceJob ):
356- # ThumbnailSourceJob needs a metadata object as well
358+ data = {"client" : json .dumps (self .clientjson )}
359+ if isinstance (job , ThumbnailJob | ThumbnailSourceJob | ImageConversionJob ):
360+ # Image generating jobs needs a metadata object as well
357361 data ["metadata" ] = json .dumps (metadata )
358362 # doit
359363 r = self .client .post (
@@ -363,7 +367,7 @@ def upload_job_result(
363367 ).raise_for_status ()
364368 t = time .time () - start
365369 logger .debug (f"Done, it took { t } seconds to upload { size } bytes, speed { round (size / t )} bytes/sec" )
366- return r .json ()
370+ return r .json () # type: ignore[no-any-return]
367371
368372 def get_exif (self , fname : Path ) -> "ExifExtractionJobResult" :
369373 """Return a dict with exif data as read by exifread from the file.
@@ -398,27 +402,9 @@ def create_album(self, file_uuids: list[uuid.UUID], title: str, description: str
398402 "description" : description ,
399403 }
400404 r = self .client .post (url , json = data ).raise_for_status ()
401- return r .json ()["bma_response" ]
405+ return r .json ()["bma_response" ] # type: ignore[no-any-return]
402406
403407 def create_thumbnail_source (self , job : ThumbnailSourceJob ) -> "ThumbnailSourceJobResult" :
404408 """Create a thumbnail source for this file."""
405- info = self .get_file_info (file_uuid = job .basefile_uuid )
406- if info ["filetype" ] == "image" :
407- # use a max 500px wide version of the image as thumbnail source
408- path = self .path / info ["filename" ]
409- original_ratio = Fraction (int (info ["width" ]), int (info ["height" ]))
410- height = math .floor (500 / original_ratio )
411- # just call the regular image conversion method to make a thumbnail
412- return self .handle_image_conversion_job (
413- job = ImageConversionJob (
414- ** job .__dict__ ,
415- width = 500 ,
416- height = height ,
417- custom_aspect_ratio = False ,
418- filetype = "WEBP" ,
419- mimetype = "image/webp" ,
420- ),
421- orig = path ,
422- )
423409 # unsupported filetype
424410 raise JobNotSupportedError (job = job )
0 commit comments