streamlit-octostar-utils 0.4.2.dev1__py3-none-any.whl → 0.4.2.dev2__py3-none-any.whl

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.
@@ -11,6 +11,7 @@ Provides a FileIO-like interface for handling entity contents with support for:
11
11
  Storage Backends:
12
12
  - MemoryContents: In-memory buffer (like BytesIO)
13
13
  - WorkspaceAttachmentContents: Octostar workspace attachments with HTTP Range support
14
+ - TemporaryAttachmentContents: Octostar temporary blob storage (user temp bucket)
14
15
  """
15
16
 
16
17
  from abc import ABC, abstractmethod
@@ -29,6 +30,7 @@ class ContentsLocation(Enum):
29
30
  """Enumeration of supported content storage locations."""
30
31
  MEMORY = "memory"
31
32
  WORKSPACE_ATTACHMENT = "workspace_attachment"
33
+ TEMPORARY_ATTACHMENT = "temporary_attachment"
32
34
 
33
35
 
34
36
  class Contents(ABC):
@@ -42,12 +44,10 @@ class Contents(ABC):
42
44
 
43
45
  def __init__(
44
46
  self,
45
- mode: str = "rb",
46
47
  entity_type: Optional[str] = None,
47
48
  filetype: Optional[str] = None,
48
49
  **kwargs
49
50
  ):
50
- self._mode = mode
51
51
  self._entity_type = entity_type
52
52
  self._filetype = filetype
53
53
  self._closed = False
@@ -105,15 +105,12 @@ class Contents(ABC):
105
105
  pass
106
106
 
107
107
  def readable(self) -> bool:
108
- """Check if stream is readable."""
109
- return 'r' in self._mode or '+' in self._mode
108
+ return True
110
109
 
111
110
  def writable(self) -> bool:
112
- """Check if stream is writable."""
113
- return 'w' in self._mode or 'a' in self._mode or '+' in self._mode
111
+ return True
114
112
 
115
113
  def seekable(self) -> bool:
116
- """Check if stream is seekable."""
117
114
  return True
118
115
 
119
116
  @abstractmethod
@@ -126,16 +123,16 @@ class Contents(ABC):
126
123
  """Close the stream and release resources."""
127
124
  self._closed = True
128
125
 
126
+ @abstractmethod
127
+ def delete(self):
128
+ """Delete the contents from their storage backend."""
129
+ pass
130
+
129
131
  @property
130
132
  def closed(self) -> bool:
131
133
  """Check if stream is closed."""
132
134
  return self._closed
133
135
 
134
- @property
135
- def mode(self) -> str:
136
- """Get the file mode."""
137
- return self._mode
138
-
139
136
  @abstractmethod
140
137
  def truncate(self, size: Optional[int] = None) -> int:
141
138
  """
@@ -286,6 +283,8 @@ class Contents(ABC):
286
283
  return MemoryContents._from_locator(locator)
287
284
  case ContentsLocation.WORKSPACE_ATTACHMENT.value:
288
285
  return WorkspaceAttachmentContents._from_locator(locator, client)
286
+ case ContentsLocation.TEMPORARY_ATTACHMENT.value:
287
+ return TemporaryAttachmentContents._from_locator(locator, client)
289
288
  case _:
290
289
  raise ValueError(f"Unknown contents location type: {location}")
291
290
 
@@ -336,24 +335,19 @@ class MemoryContents(Contents):
336
335
 
337
336
  def __init__(
338
337
  self,
339
- mode: str = "r+b",
340
338
  entity_type: Optional[str] = None,
341
339
  filetype: Optional[str] = None,
342
340
  *,
343
341
  initial_data: Optional[bytes] = None,
344
342
  **kwargs
345
343
  ):
346
- super().__init__(mode, entity_type, filetype, **kwargs)
344
+ super().__init__(entity_type, filetype, **kwargs)
347
345
  self._buffer = BytesIO(initial_data or b"")
348
346
 
349
347
  def read(self, size: int = -1) -> bytes:
350
- if not self.readable():
351
- raise IOError("Contents not readable")
352
348
  return self._buffer.read(size)
353
349
 
354
350
  def write(self, b: bytes) -> int:
355
- if not self.writable():
356
- raise IOError("Contents not writable")
357
351
  return self._buffer.write(b)
358
352
 
359
353
  def seek(self, offset: int, whence: int = SEEK_SET) -> int:
@@ -370,6 +364,10 @@ class MemoryContents(Contents):
370
364
  self._buffer.close()
371
365
  super().close()
372
366
 
367
+ def delete(self):
368
+ """Clear the in-memory buffer."""
369
+ self._buffer = BytesIO(b"")
370
+
373
371
  def truncate(self, size: Optional[int] = None) -> int:
374
372
  return self._buffer.truncate(size)
375
373
 
@@ -435,7 +433,6 @@ class WorkspaceAttachmentContents(Contents):
435
433
 
436
434
  def __init__(
437
435
  self,
438
- mode: str = "rb",
439
436
  entity_type: Optional[str] = None,
440
437
  filetype: Optional[str] = None,
441
438
  *,
@@ -446,7 +443,7 @@ class WorkspaceAttachmentContents(Contents):
446
443
  chunk_size: int = DEFAULT_CHUNK_SIZE,
447
444
  **kwargs
448
445
  ):
449
- super().__init__(mode, entity_type, filetype, **kwargs)
446
+ super().__init__(entity_type, filetype, **kwargs)
450
447
  self._workspace_id = workspace_id
451
448
  self._entity_id = entity_id
452
449
  self._client = client
@@ -575,11 +572,7 @@ class WorkspaceAttachmentContents(Contents):
575
572
  self._position = 0
576
573
 
577
574
  def read(self, size: int = -1) -> bytes:
578
- if not self.readable():
579
- raise IOError("Contents not readable")
580
-
581
- # If writable or already fully loaded, use buffer
582
- if self.writable() or self._fully_loaded:
575
+ if self._fully_loaded:
583
576
  if not self._buffer:
584
577
  self._load_full()
585
578
  return self._buffer.read(size)
@@ -613,9 +606,6 @@ class WorkspaceAttachmentContents(Contents):
613
606
  return data
614
607
 
615
608
  def write(self, b: bytes) -> int:
616
- if not self.writable():
617
- raise IOError("Contents not writable")
618
-
619
609
  if not self._buffer:
620
610
  self._load_full()
621
611
 
@@ -684,10 +674,8 @@ class WorkspaceAttachmentContents(Contents):
684
674
 
685
675
  def close(self):
686
676
  if not self._closed:
687
- # Flush any pending writes before closing
688
677
  if self._modified:
689
678
  self.flush()
690
-
691
679
  if self._buffer:
692
680
  self._buffer.close()
693
681
  if self._http_client:
@@ -695,6 +683,15 @@ class WorkspaceAttachmentContents(Contents):
695
683
  self._http_client = None
696
684
  super().close()
697
685
 
686
+ def delete(self):
687
+ """Delete the entity from the workspace using delete_entity()."""
688
+ from octostar.utils.workspace import delete_entity
689
+
690
+ delete_entity.sync(
691
+ os_entity_uid=self._entity_id,
692
+ client=self._client
693
+ )
694
+
698
695
  def truncate(self, size: Optional[int] = None) -> int:
699
696
  if not self._buffer:
700
697
  self._load_full()
@@ -758,3 +755,160 @@ class WorkspaceAttachmentContents(Contents):
758
755
  entity_id=entity_id,
759
756
  client=client
760
757
  )
758
+
759
+
760
+ class TemporaryAttachmentContents(Contents):
761
+ """
762
+ Contents implementation for Octostar temporary blob storage.
763
+
764
+ Uses octostar-api utilities (read_temporary_blob, write_temporary_blob,
765
+ delete_temporary_blob) to store files in the user's temporary S3 bucket.
766
+
767
+ Temporary blobs are keyed by filename (not workspace/entity), and are not
768
+ associated with any workspace entity. Use WorkspaceAttachmentContents for that.
769
+ """
770
+
771
+ def __init__(
772
+ self,
773
+ entity_type: Optional[str] = None,
774
+ filetype: Optional[str] = None,
775
+ *,
776
+ filename: str,
777
+ client,
778
+ initial_data: Optional[bytes] = None,
779
+ **kwargs
780
+ ):
781
+ super().__init__(entity_type, filetype, **kwargs)
782
+ self._filename = filename
783
+ self._client = client
784
+
785
+ self._buffer: Optional[BytesIO] = None
786
+ self._fully_loaded = False
787
+ self._modified = False
788
+
789
+ if initial_data is not None:
790
+ self._buffer = BytesIO(initial_data)
791
+ self._fully_loaded = True
792
+
793
+ def _load_full(self):
794
+ """Load the entire blob into memory using read_temporary_blob()."""
795
+ if self._fully_loaded:
796
+ return
797
+
798
+ from octostar.utils.workspace import read_temporary_blob
799
+
800
+ data = read_temporary_blob.sync(
801
+ filename=self._filename,
802
+ decode=False,
803
+ client=self._client
804
+ )
805
+ self._buffer = BytesIO(data or b"")
806
+ self._fully_loaded = True
807
+
808
+ def read(self, size: int = -1) -> bytes:
809
+ if not self._buffer:
810
+ self._load_full()
811
+ return self._buffer.read(size)
812
+
813
+ def write(self, b: bytes) -> int:
814
+ if not self._buffer:
815
+ self._load_full()
816
+ n = self._buffer.write(b)
817
+ self._modified = True
818
+ return n
819
+
820
+ def seek(self, offset: int, whence: int = SEEK_SET) -> int:
821
+ if not self._buffer:
822
+ self._load_full()
823
+ return self._buffer.seek(offset, whence)
824
+
825
+ def tell(self) -> int:
826
+ if not self._buffer:
827
+ self._load_full()
828
+ return self._buffer.tell()
829
+
830
+ def flush(self):
831
+ """Flush the internal buffer and write to temp bucket if modified."""
832
+ if self._buffer:
833
+ self._buffer.flush()
834
+
835
+ if not self._modified or not self._buffer:
836
+ return
837
+
838
+ from octostar.utils.workspace import write_temporary_blob
839
+
840
+ current_pos = self._buffer.tell()
841
+ self._buffer.seek(0, SEEK_SET)
842
+ data = self._buffer.read()
843
+ self._buffer.seek(current_pos, SEEK_SET)
844
+
845
+ write_temporary_blob.sync(
846
+ filename=self._filename,
847
+ file=data,
848
+ client=self._client
849
+ )
850
+ self._modified = False
851
+
852
+ def close(self):
853
+ if not self._closed:
854
+ if self._modified:
855
+ self.flush()
856
+ if self._buffer:
857
+ self._buffer.close()
858
+ super().close()
859
+
860
+ def delete(self):
861
+ """Delete the blob from the temporary bucket."""
862
+ from octostar.utils.workspace import delete_temporary_blob
863
+
864
+ delete_temporary_blob.sync(
865
+ filename=self._filename,
866
+ client=self._client
867
+ )
868
+
869
+ def truncate(self, size: Optional[int] = None) -> int:
870
+ if not self._buffer:
871
+ self._load_full()
872
+ self._modified = True
873
+ return self._buffer.truncate(size)
874
+
875
+ def getvalue(self) -> bytes:
876
+ if not self._buffer or not self._fully_loaded:
877
+ self._load_full()
878
+ return self._buffer.getvalue()
879
+
880
+ def to_locator(self) -> Dict[str, Any]:
881
+ """
882
+ Serialize to locator with filename.
883
+
884
+ Returns:
885
+ {"location": "temporary_attachment", "filename": "..."}
886
+ """
887
+ locator = {
888
+ "location": ContentsLocation.TEMPORARY_ATTACHMENT.value,
889
+ "filename": self._filename
890
+ }
891
+ if self._entity_type:
892
+ locator["entity_type"] = self._entity_type
893
+ if self._filetype:
894
+ locator["filetype"] = self._filetype
895
+ return locator
896
+
897
+ @staticmethod
898
+ def _from_locator(locator: Dict[str, Any], client=None) -> 'TemporaryAttachmentContents':
899
+ """
900
+ Create TemporaryAttachmentContents from a locator dictionary.
901
+
902
+ Args:
903
+ locator: Locator dictionary with filename
904
+ client: Octostar client
905
+
906
+ Returns:
907
+ New TemporaryAttachmentContents instance
908
+ """
909
+ return TemporaryAttachmentContents(
910
+ entity_type=locator.get("entity_type"),
911
+ filetype=locator.get("filetype"),
912
+ filename=locator["filename"],
913
+ client=client
914
+ )
@@ -26,7 +26,7 @@ from ..core.dict import recursive_update_dict, travel_dict, jsondict_hash
26
26
  from ..core.timestamp import now, string_to_datetime
27
27
  from .fastapi import DefaultErrorRoute, Route
28
28
  from ..ontology.inheritance import is_child_concept as is_child_concept_fn, get_label_keys
29
- from .contents import Contents, MemoryContents, WorkspaceAttachmentContents, ContentsLocation
29
+ from .contents import Contents, MemoryContents, WorkspaceAttachmentContents, TemporaryAttachmentContents, ContentsLocation
30
30
 
31
31
  RELATIONSHIP_ENTITY_NAME = "os_relationship"
32
32
  LOCAL_RELATIONSHIP_ENTITY_NAME = "os_workspace_relationship"
@@ -453,11 +453,14 @@ class NifiContextManager(object):
453
453
  fetch_concept_relationships = {}
454
454
  # FIND FILES TO WRITE
455
455
  for entity in entities:
456
- if entity.is_child_concept("os_file"):
456
+ if entity.is_child_concept("os_attachable"):
457
457
  has_write_flag = entity.sync_params.get(NifiContextManager.SyncFlag.WRITE_CONTENTS)
458
- is_temp_with_pointer = entity.request.get("is_temporary") and entity.contents_pointer
459
- if has_write_flag or is_temp_with_pointer:
460
- if entity.contents: # Contents instance check
458
+ is_temporary = entity.request.get("is_temporary")
459
+ if is_temporary:
460
+ if entity._contents and not isinstance(entity._contents, WorkspaceAttachmentContents):
461
+ files_to_write.append(entity)
462
+ elif has_write_flag:
463
+ if entity.contents:
461
464
  files_to_write.append(entity)
462
465
  # FIND ENTITIES TO UPSERT
463
466
  self._find_entities_to_upsert(entities, entities_to_upsert, reserved_fields)
@@ -487,8 +490,7 @@ class NifiContextManager(object):
487
490
  for file in files_to_write:
488
491
  if not file.contents:
489
492
  continue
490
-
491
- # Pass Contents instance directly — write_file uses duck typing
493
+ old_contents = file._contents
492
494
  new_file_record = write_file.sync(
493
495
  file.write_os_workspace,
494
496
  "./" + file.record["os_item_name"],
@@ -517,6 +519,11 @@ class NifiContextManager(object):
517
519
  "entity_type": file.record["os_concept"],
518
520
  "filetype": file.record["os_item_content_type"]
519
521
  }
522
+ if isinstance(old_contents, TemporaryAttachmentContents):
523
+ try:
524
+ old_contents.delete()
525
+ except Exception:
526
+ pass
520
527
  # UPSERT ENTITIES
521
528
  if entities_to_upsert:
522
529
  new_entities = upsert_entities.sync(
@@ -1062,9 +1069,16 @@ class NifiEntity(object):
1062
1069
  if isinstance(file, Contents):
1063
1070
  child_entity._contents = file
1064
1071
  else:
1065
- child_entity._contents = MemoryContents(
1066
- entity_type=FILE_ENTITY_NAME, filetype=filetype, initial_data=file
1072
+ temp_filename = f"tmp_{child_entity.record['os_entity_uid']}"
1073
+ temp_contents = TemporaryAttachmentContents(
1074
+ entity_type=FILE_ENTITY_NAME,
1075
+ filetype=filetype,
1076
+ filename=temp_filename,
1077
+ client=self.context.client,
1078
+ initial_data=file,
1067
1079
  )
1080
+ temp_contents.flush()
1081
+ child_entity._contents = temp_contents
1068
1082
  child_entity.request["contents_pointer"] = child_entity._contents.to_locator()
1069
1083
  return child_entity, child_rel
1070
1084
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamlit-octostar-utils
3
- Version: 0.4.2.dev1
3
+ Version: 0.4.2.dev2
4
4
  Summary:
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,9 +1,9 @@
1
1
  streamlit_octostar_utils/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
2
  streamlit_octostar_utils/api_crafter/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
3
3
  streamlit_octostar_utils/api_crafter/celery.py,sha256=IFxeJlxn1C6bP-7bhRlHMGhRJY0Ab8TwB2eUj1DLjaM,33501
4
- streamlit_octostar_utils/api_crafter/contents.py,sha256=QIcV--VR9inbSWkyR1Qr_imdh8RUPJlx9HWctBlb6m8,24431
4
+ streamlit_octostar_utils/api_crafter/contents.py,sha256=htnCnDqnLx2o0dIk2xioLJ3SSzhqm3n0viJsxyG78pk,29273
5
5
  streamlit_octostar_utils/api_crafter/fastapi.py,sha256=7J_GqXouvX0Y7pIpsgO5q14StJDArtGdXhTAvXeIjGI,14372
6
- streamlit_octostar_utils/api_crafter/nifi.py,sha256=WTEWO1pQQo7dYA1fjM8e2hKawxilbiR-BV6uNNrBMo4,47691
6
+ streamlit_octostar_utils/api_crafter/nifi.py,sha256=He5Hjd0QpUQmE8WF0wXu9WY5xsWMtZOd0JxKbixSFtw,48290
7
7
  streamlit_octostar_utils/api_crafter/parallelism.py,sha256=YDQ5aQ6w4bwI_UAydQOOLYQr3svuweMAH-Jdf5jpYPI,29388
8
8
  streamlit_octostar_utils/api_crafter/parser/__init__.py,sha256=YeYWF6sdQiCFV_RKNW2t9Vs6KJExE2pbXxWTe_DOayY,107
9
9
  streamlit_octostar_utils/api_crafter/parser/combine_fields.py,sha256=ddc44xkajw8MU0peAX_263DL7rPXbTKbHUjpOhRgvyU,8790
@@ -39,7 +39,7 @@ streamlit_octostar_utils/threading/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzp
39
39
  streamlit_octostar_utils/threading/async_task_manager.py,sha256=q7N6YZwUvIYMzkSHmsJNheNVCv93c03H6Hyg9uH8pvk,4747
40
40
  streamlit_octostar_utils/threading/session_callback_manager.py,sha256=LvZVP4g6tvKtYmI13f2j1sX_7hm61Groqp5xJine9_k,3973
41
41
  streamlit_octostar_utils/threading/session_state_hot_swapper.py,sha256=6eeCQI6A42hp4DmW2NQw2rbeR-k9N8DhfBKQdN_fbLU,811
42
- streamlit_octostar_utils-0.4.2.dev1.dist-info/METADATA,sha256=GgxzBi03rid-K4rP9j-8nGo0tV4zxJlKVbAZx0JZWl4,2413
43
- streamlit_octostar_utils-0.4.2.dev1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
44
- streamlit_octostar_utils-0.4.2.dev1.dist-info/licenses/LICENSE,sha256=dkwVPyV03fPHHtERnF6RnvRXcll__tud9gWca1RcgnQ,1073
45
- streamlit_octostar_utils-0.4.2.dev1.dist-info/RECORD,,
42
+ streamlit_octostar_utils-0.4.2.dev2.dist-info/METADATA,sha256=5sx_MMyva6Wx2TZY_5IEhlrRnQh0RMweOzokzTv4ua8,2413
43
+ streamlit_octostar_utils-0.4.2.dev2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
44
+ streamlit_octostar_utils-0.4.2.dev2.dist-info/licenses/LICENSE,sha256=dkwVPyV03fPHHtERnF6RnvRXcll__tud9gWca1RcgnQ,1073
45
+ streamlit_octostar_utils-0.4.2.dev2.dist-info/RECORD,,