streamlit-octostar-utils 0.4.2.dev1__tar.gz → 0.4.2.dev3__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.
Files changed (45) hide show
  1. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/PKG-INFO +1 -1
  2. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/pyproject.toml +1 -1
  3. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/contents.py +184 -30
  4. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/nifi.py +77 -50
  5. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/LICENSE +0 -0
  6. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/README.md +0 -0
  7. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/__init__.py +0 -0
  8. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/__init__.py +0 -0
  9. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/celery.py +0 -0
  10. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/fastapi.py +0 -0
  11. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parallelism.py +0 -0
  12. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/__init__.py +0 -0
  13. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/combine_fields.py +0 -0
  14. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/entities_parser.py +0 -0
  15. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/generics.py +0 -0
  16. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/info.py +0 -0
  17. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/linkchart_functions.py +0 -0
  18. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/matches.py +0 -0
  19. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/parameters.py +0 -0
  20. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/rules.py +0 -0
  21. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/api_crafter/parser/signals.py +0 -0
  22. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/core/__init__.py +0 -0
  23. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/core/dict.py +0 -0
  24. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/core/filetypes.py +0 -0
  25. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/core/threading/__init__.py +0 -0
  26. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/core/threading/key_queue.py +0 -0
  27. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/core/timestamp.py +0 -0
  28. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/nlp/__init__.py +0 -0
  29. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/nlp/custom_recognizers.py +0 -0
  30. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/nlp/language.py +0 -0
  31. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/nlp/ner.py +0 -0
  32. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/octostar/__init__.py +0 -0
  33. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/octostar/client.py +0 -0
  34. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/octostar/context.py +0 -0
  35. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/octostar/permissions.py +0 -0
  36. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/ontology/__init__.py +0 -0
  37. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/ontology/inheritance.py +0 -0
  38. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/ontology/relationships.py +0 -0
  39. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/ontology/validation.py +0 -0
  40. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/style/__init__.py +0 -0
  41. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/style/common.py +0 -0
  42. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/threading/__init__.py +0 -0
  43. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/threading/async_task_manager.py +0 -0
  44. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/threading/session_callback_manager.py +0 -0
  45. {streamlit_octostar_utils-0.4.2.dev1 → streamlit_octostar_utils-0.4.2.dev3}/streamlit_octostar_utils/threading/session_state_hot_swapper.py +0 -0
@@ -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.dev3
4
4
  Summary:
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -5,7 +5,7 @@ include = '\.pyi?$'
5
5
 
6
6
  [tool.poetry]
7
7
  name = "streamlit-octostar-utils"
8
- version = "0.4.2-dev.1"
8
+ version = "0.4.2-dev.3"
9
9
  description = ""
10
10
  license = "MIT"
11
11
  authors = ["Octostar"]
@@ -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
+ )
@@ -5,18 +5,16 @@ from contextlib import contextmanager
5
5
  import json
6
6
  import jwt
7
7
  from typing import List, Union, Optional, Callable
8
- import base64
9
8
  from pydantic import BaseModel, ConfigDict, Field
10
9
  import itertools
11
- import asyncio
12
10
  from enum import Enum
13
- from typing import Dict, Any
11
+ from typing import Dict, Any, Literal
14
12
  from datetime import datetime, timezone, timedelta
15
13
  from fastapi import Request
16
14
  from fastapi.responses import JSONResponse
17
15
  from starlette.exceptions import HTTPException as StarletteHTTPException
18
16
 
19
- from octostar.utils.workspace import read_file, upsert_entities, write_file
17
+ from octostar.utils.workspace import upsert_entities, write_file
20
18
  from octostar.utils.ontology import fetch_ontology_data
21
19
  from octostar.utils.workspace.permissions import get_permissions, PermissionLevel
22
20
  from octostar.client import make_client
@@ -26,26 +24,23 @@ from ..core.dict import recursive_update_dict, travel_dict, jsondict_hash
26
24
  from ..core.timestamp import now, string_to_datetime
27
25
  from .fastapi import DefaultErrorRoute, Route
28
26
  from ..ontology.inheritance import is_child_concept as is_child_concept_fn, get_label_keys
29
- from .contents import Contents, MemoryContents, WorkspaceAttachmentContents, ContentsLocation
27
+ from .contents import Contents, WorkspaceAttachmentContents, TemporaryAttachmentContents, ContentsLocation
28
+
29
+
30
+ class RelationshipName(BaseModel):
31
+ name: str
32
+ type: Literal["mtm", "otm"]
33
+
30
34
 
31
35
  RELATIONSHIP_ENTITY_NAME = "os_relationship"
32
36
  LOCAL_RELATIONSHIP_ENTITY_NAME = "os_workspace_relationship"
33
37
  FILE_ENTITY_NAME = "os_file"
34
- GENERIC_RELATIONSHIP_NAME = "related_to"
35
- FILE_RELATIONSHIP_NAME = "generator_of"
36
- TAG_RELATIONSHIP_NAME = "has_tag"
37
38
  TAG_ENTITY_NAME = "os_tag"
38
-
39
-
40
- def safe_async_run(coro):
41
- try:
42
- loop = asyncio.get_event_loop()
43
- if loop.is_running():
44
- return asyncio.ensure_future(coro)
45
- else:
46
- return asyncio.run(coro)
47
- except RuntimeError:
48
- return asyncio.run(coro)
39
+ FRAGMENT_ENTITY_NAME = "os_fragment"
40
+ GENERIC_RELATIONSHIP = RelationshipName(name="related_to", type="mtm")
41
+ FILE_RELATIONSHIP = RelationshipName(name="generator_of", type="mtm")
42
+ TAG_RELATIONSHIP = RelationshipName(name="has_tag", type="mtm")
43
+ FRAGMENT_RELATIONSHIP = RelationshipName(name="is_fragment_of", type="otm")
49
44
 
50
45
 
51
46
  class NifiProxyEntityModel(BaseModel):
@@ -397,10 +392,6 @@ class NifiContextManager(object):
397
392
 
398
393
  @contextmanager
399
394
  def reindex_lock(self, entities: List[Union[dict, "NifiEntity"]], timeout: int = 7200):
400
- """Lock entities to prevent reindexing during processing.
401
-
402
- Sets do_not_reindex_at to a future time on entry, resets on exit.
403
- """
404
395
  if not entities:
405
396
  yield
406
397
  return
@@ -453,11 +444,14 @@ class NifiContextManager(object):
453
444
  fetch_concept_relationships = {}
454
445
  # FIND FILES TO WRITE
455
446
  for entity in entities:
456
- if entity.is_child_concept("os_file"):
447
+ if entity.is_child_concept("os_attachable"):
457
448
  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
449
+ is_temporary = entity.request.get("is_temporary")
450
+ if is_temporary:
451
+ if entity._contents and not isinstance(entity._contents, WorkspaceAttachmentContents):
452
+ files_to_write.append(entity)
453
+ elif has_write_flag:
454
+ if entity.contents:
461
455
  files_to_write.append(entity)
462
456
  # FIND ENTITIES TO UPSERT
463
457
  self._find_entities_to_upsert(entities, entities_to_upsert, reserved_fields)
@@ -487,8 +481,7 @@ class NifiContextManager(object):
487
481
  for file in files_to_write:
488
482
  if not file.contents:
489
483
  continue
490
-
491
- # Pass Contents instance directly — write_file uses duck typing
484
+ old_contents = file._contents
492
485
  new_file_record = write_file.sync(
493
486
  file.write_os_workspace,
494
487
  "./" + file.record["os_item_name"],
@@ -517,6 +510,11 @@ class NifiContextManager(object):
517
510
  "entity_type": file.record["os_concept"],
518
511
  "filetype": file.record["os_item_content_type"]
519
512
  }
513
+ if isinstance(old_contents, TemporaryAttachmentContents):
514
+ try:
515
+ old_contents.delete()
516
+ except Exception:
517
+ pass
520
518
  # UPSERT ENTITIES
521
519
  if entities_to_upsert:
522
520
  new_entities = upsert_entities.sync(
@@ -572,8 +570,7 @@ class NifiContextManager(object):
572
570
  target["os_workspace"],
573
571
  target["os_concept"],
574
572
  target,
575
- os_relationship_name=rel["os_relationship_name"],
576
- os_relationship_type=rel_type,
573
+ relationship=RelationshipName(name=rel["os_relationship_name"], type=rel_type),
577
574
  )
578
575
  child_entity.record = {**child_entity.record, **target}
579
576
  child_entity.record["entity_id"] = child_entity.record[
@@ -967,7 +964,7 @@ class NifiEntity(object):
967
964
  ):
968
965
  return self._add_entity(
969
966
  os_relationship_workspace,
970
- (LOCAL_RELATIONSHIP_ENTITY_NAME if os_relationship_workspace else RELATIONSHIP_ENTITY_NAME),
967
+ LOCAL_RELATIONSHIP_ENTITY_NAME,
971
968
  {
972
969
  **relationship_fields,
973
970
  "os_entity_uid_from": os_entity_uid_from,
@@ -1002,33 +999,32 @@ class NifiEntity(object):
1002
999
  os_workspace,
1003
1000
  entity_type,
1004
1001
  fields,
1005
- os_relationship_name=GENERIC_RELATIONSHIP_NAME,
1006
- os_relationship_type="mtm",
1002
+ relationship: RelationshipName = GENERIC_RELATIONSHIP,
1007
1003
  os_entity_uid=None,
1008
1004
  os_relationship_uid=None,
1009
1005
  ):
1010
1006
  child_entity = self._add_entity(os_workspace, entity_type, fields, os_entity_uid)
1011
- if os_relationship_type == "mtm":
1007
+ if relationship.type == "mtm":
1012
1008
  child_rel = self.add_mtm_relationship(
1013
1009
  self.record["os_entity_uid"],
1014
1010
  self.record["os_concept"],
1015
1011
  child_entity.record["os_entity_uid"],
1016
1012
  child_entity.record["os_concept"],
1017
- os_relationship_name,
1013
+ relationship.name,
1018
1014
  os_workspace,
1019
1015
  {},
1020
1016
  os_relationship_uid,
1021
1017
  )
1022
- elif os_relationship_type == "otm":
1018
+ elif relationship.type == "otm":
1023
1019
  child_rel = self.add_otm_relationship(
1024
1020
  self.record["os_entity_uid"],
1025
1021
  self.record["os_concept"],
1026
1022
  child_entity.record["os_entity_uid"],
1027
1023
  child_entity.record["os_concept"],
1028
- os_relationship_name,
1024
+ relationship.name,
1029
1025
  )
1030
1026
  else:
1031
- raise ValueError(f"os_relationship_type is invalid! {os_relationship_type}")
1027
+ raise ValueError(f"relationship.type is invalid! {relationship.type}")
1032
1028
  return child_entity, child_rel
1033
1029
 
1034
1030
  def add_child_file(
@@ -1039,14 +1035,14 @@ class NifiEntity(object):
1039
1035
  filetype,
1040
1036
  file: Union[Contents, bytes],
1041
1037
  fields={},
1042
- os_relationship_name=FILE_RELATIONSHIP_NAME,
1043
- os_relationship_type="mtm",
1038
+ relationship: RelationshipName = FILE_RELATIONSHIP,
1044
1039
  os_entity_uid=None,
1045
1040
  os_relationship_uid=None,
1041
+ os_entity_type=FILE_ENTITY_NAME,
1046
1042
  ):
1047
1043
  child_entity, child_rel = self.add_child_entity(
1048
1044
  os_workspace,
1049
- FILE_ENTITY_NAME,
1045
+ os_entity_type,
1050
1046
  {
1051
1047
  **fields,
1052
1048
  "os_item_name": filename,
@@ -1054,27 +1050,58 @@ class NifiEntity(object):
1054
1050
  "os_has_attachment": True,
1055
1051
  "os_parent_folder": os_parent_folder,
1056
1052
  },
1057
- os_relationship_name,
1058
- os_relationship_type,
1053
+ relationship,
1059
1054
  os_entity_uid,
1060
1055
  os_relationship_uid,
1061
1056
  )
1062
1057
  if isinstance(file, Contents):
1063
1058
  child_entity._contents = file
1064
1059
  else:
1065
- child_entity._contents = MemoryContents(
1066
- entity_type=FILE_ENTITY_NAME, filetype=filetype, initial_data=file
1060
+ temp_filename = f"tmp_{child_entity.record['os_entity_uid']}"
1061
+ temp_contents = TemporaryAttachmentContents(
1062
+ entity_type=os_entity_type,
1063
+ filetype=filetype,
1064
+ filename=temp_filename,
1065
+ client=self.context.client,
1066
+ initial_data=file,
1067
1067
  )
1068
+ temp_contents.flush()
1069
+ child_entity._contents = temp_contents
1068
1070
  child_entity.request["contents_pointer"] = child_entity._contents.to_locator()
1069
1071
  return child_entity, child_rel
1070
1072
 
1073
+ def add_child_fragment(
1074
+ self,
1075
+ os_workspace,
1076
+ os_parent_folder,
1077
+ filename,
1078
+ filetype,
1079
+ file: Union[Contents, bytes],
1080
+ fields={},
1081
+ relationship: RelationshipName = FRAGMENT_RELATIONSHIP,
1082
+ os_entity_uid=None,
1083
+ os_relationship_uid=None,
1084
+ os_entity_type=FRAGMENT_ENTITY_NAME,
1085
+ ):
1086
+ return self.add_child_file(
1087
+ os_workspace,
1088
+ os_parent_folder,
1089
+ filename,
1090
+ filetype,
1091
+ file,
1092
+ fields,
1093
+ relationship,
1094
+ os_entity_uid,
1095
+ os_relationship_uid,
1096
+ os_entity_type,
1097
+ )
1098
+
1071
1099
  def add_tag(self, os_workspace, name, group, order, color, fields={}):
1072
1100
  return self.add_child_entity(
1073
1101
  os_workspace,
1074
- "os_tag",
1075
- {**fields,"name": name, "group": group, "order": order, "color": color},
1076
- TAG_RELATIONSHIP_NAME,
1077
- "mtm",
1102
+ TAG_ENTITY_NAME,
1103
+ {**fields, "name": name, "group": group, "order": order, "color": color},
1104
+ TAG_RELATIONSHIP,
1078
1105
  )
1079
1106
 
1080
1107
  def add_annotations(