1818import msgpack
1919import pydantic
2020
21+ from collections .abc import Generator
2122from simvue .utilities import staging_merger
2223from simvue .config .user import SimvueConfiguration
2324from simvue .exception import ObjectNotFoundError
@@ -172,6 +173,16 @@ def to_params(self) -> dict[str, str]:
172173 return {"id" : self .column , "desc" : self .descending }
173174
174175
176+ class VisibilityBatchArgs (pydantic .BaseModel ):
177+ tenant : bool | None = None
178+ user : list [str ] | None = None
179+ public : bool | None = None
180+
181+
182+ class ObjectBatchArgs (pydantic .BaseModel ):
183+ pass
184+
185+
175186class SimvueObject (abc .ABC ):
176187 def __init__ (
177188 self ,
@@ -361,13 +372,21 @@ def _get_visibility(self) -> dict[str, bool | list[str]]:
361372 return {}
362373
363374 @classmethod
375+ @abc .abstractmethod
364376 def new (cls , ** _ ) -> Self :
365377 pass
366378
379+ @classmethod
380+ def batch_create (
381+ cls , obj_args : ObjectBatchArgs , visibility : VisibilityBatchArgs
382+ ) -> Generator [str ]:
383+ _ , __ = obj_args , visibility
384+ raise NotImplementedError
385+
367386 @classmethod
368387 def ids (
369388 cls , count : int | None = None , offset : int | None = None , ** kwargs
370- ) -> typing . Generator [str , None , None ]:
389+ ) -> Generator [str , None , None ]:
371390 """Retrieve a list of all object identifiers.
372391
373392 Parameters
@@ -402,7 +421,7 @@ def get(
402421 count : pydantic .PositiveInt | None = None ,
403422 offset : pydantic .NonNegativeInt | None = None ,
404423 ** kwargs ,
405- ) -> typing . Generator [tuple [str , T | None ], None , None ]:
424+ ) -> Generator [tuple [str , T | None ], None , None ]:
406425 """Retrieve items of this object type from the server.
407426
408427 Parameters
@@ -467,7 +486,7 @@ def _get_all_objects(
467486 endpoint : str | None = None ,
468487 expected_type : type = dict ,
469488 ** kwargs ,
470- ) -> typing . Generator [dict , None , None ]:
489+ ) -> Generator [dict , None , None ]:
471490 _class_instance = cls (_read_only = True )
472491
473492 # Allow the possibility of paginating a URL that is not the
@@ -514,7 +533,7 @@ def read_only(self, is_read_only: bool) -> None:
514533 if not self ._read_only :
515534 self ._staging = self ._get_local_staged ()
516535
517- def commit (self ) -> dict | None :
536+ def commit (self ) -> dict | list [ dict ] | None :
518537 """Send updates to the server, or if offline, store locally."""
519538 if self ._read_only :
520539 raise AttributeError ("Cannot commit object in 'read-only' mode" )
@@ -526,15 +545,22 @@ def commit(self) -> dict | None:
526545 self ._cache ()
527546 return
528547
529- _response : dict | None = None
548+ _response : dict [ str , str ] | list [ dict [ str , str ]] | None = None
530549
531550 # Initial commit is creation of object
532551 # if staging is empty then we do not need to use PUT
533552 if not self ._identifier or self ._identifier .startswith ("offline_" ):
534- self ._logger .debug (
535- f"Posting from staged data for { self ._label } '{ self .id } ': { self ._staging } "
536- )
537- _response = self ._post (** self ._staging )
553+ # If batch upload send as list, else send as dictionary of params
554+ if _batch_commit := self ._staging .get ("batch" ):
555+ self ._logger .debug (
556+ f"Posting batched data to server: { len (_batch_commit )} { self ._label } s"
557+ )
558+ _response = self ._post_batch (batch_data = _batch_commit )
559+ else :
560+ self ._logger .debug (
561+ f"Posting from staged data for { self ._label } '{ self .id } ': { self ._staging } "
562+ )
563+ _response = self ._post_single (** self ._staging )
538564 elif self ._staging :
539565 self ._logger .debug (
540566 f"Pushing updates from staged data for { self ._label } '{ self .id } ': { self ._staging } "
@@ -570,11 +596,45 @@ def url(self) -> URL | None:
570596 """
571597 return None if self ._identifier is None else self ._base_url / self ._identifier
572598
573- def _post (
599+ def _post_batch (
600+ self ,
601+ batch_data : list [ObjectBatchArgs ],
602+ ) -> list [dict [str , str ]]:
603+ _response = sv_post (
604+ url = f"{ self ._base_url } " ,
605+ headers = self ._headers | {"Content-Type" : "application/msgpack" },
606+ params = self ._params ,
607+ data = batch_data ,
608+ is_json = True ,
609+ )
610+
611+ if _response .status_code == http .HTTPStatus .FORBIDDEN :
612+ raise RuntimeError (
613+ f"Forbidden: You do not have permission to create object of type '{ self ._label } '"
614+ )
615+
616+ _json_response = get_json_from_response (
617+ response = _response ,
618+ expected_status = [http .HTTPStatus .OK , http .HTTPStatus .CONFLICT ],
619+ scenario = f"Creation of multiple { self ._label } s" ,
620+ expected_type = list ,
621+ )
622+
623+ if not len (batch_data ) == (_n_created := len (_json_response )):
624+ raise RuntimeError (
625+ f"Expected { len (batch_data )} to be created, but only { _n_created } found."
626+ )
627+
628+ self ._logger .debug (f"successfully created { _n_created } { self ._label } s" )
629+
630+ return _json_response
631+
632+ def _post_single (
574633 self , * , is_json : bool = True , data : list | dict | None = None , ** kwargs
575- ) -> dict [str , typing .Any ]:
634+ ) -> dict [str , typing .Any ] | list [ dict [ str , typing . Any ]] :
576635 if not is_json :
577636 kwargs = msgpack .packb (data or kwargs , use_bin_type = True )
637+
578638 _response = sv_post (
579639 url = f"{ self ._base_url } " ,
580640 headers = self ._headers | {"Content-Type" : "application/msgpack" },
@@ -594,11 +654,6 @@ def _post(
594654 scenario = f"Creation of { self ._label } " ,
595655 )
596656
597- if isinstance (_json_response , list ):
598- raise RuntimeError (
599- "Expected dictionary from JSON response but got type list"
600- )
601-
602657 if _id := _json_response .get ("id" ):
603658 self ._logger .debug ("'%s' created successfully" , _id )
604659 self ._identifier = _id
0 commit comments