datashare-python 0.11.2__py3-none-any.whl → 0.11.4__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.
datashare_python/utils.py CHANGED
@@ -10,6 +10,7 @@ import sys
10
10
  import threading
11
11
  import time
12
12
  from collections.abc import (
13
+ AsyncGenerator,
13
14
  AsyncIterable,
14
15
  Awaitable,
15
16
  Callable,
@@ -522,7 +523,7 @@ def _write_artifact_bytes(path: Path, artifact: bytes | BytesIO | Path) -> None:
522
523
  def _remove_previous_artifacts(path: Path) -> None:
523
524
  if isinstance(path, Path) and path.exists():
524
525
  with contextlib.suppress(FileNotFoundError):
525
- path.rmdir()
526
+ shutil.rmtree(path)
526
527
  else:
527
528
  path.unlink(missing_ok=True)
528
529
 
@@ -766,59 +767,108 @@ class SharedResources:
766
767
  eviction_callback: Callable[[Any, Any], None] | None = None,
767
768
  ) -> None:
768
769
  self._eviction_callback = eviction_callback
769
- self._cache = LRU(cache_size, self._eviction_callback)
770
+ self._cache = LRU(cache_size, self._on_evict)
770
771
  self._sentinel = object()
772
+ # Leased resources are tracked by id, evicting a leased resource defers the
773
+ # eviction callback until its last lease is released
774
+ self._lock = threading.Lock()
775
+ self._n_leases: dict[int, int] = dict()
776
+ self._pending_evictions: dict[int, tuple[Any, Any]] = dict()
771
777
 
772
778
  def __enter__(self) -> Self:
773
779
  return self
774
780
 
775
781
  def __exit__(self, exc_type, exc_val, exc_tb) -> None: # noqa: ANN001
776
- if self._eviction_callback is not None:
777
- for k, v in self._cache.items():
778
- self._eviction_callback(k, v)
782
+ for k, v in self._cache.items():
783
+ self._on_evict(k, v)
779
784
 
785
+ @contextlib.contextmanager
780
786
  def get_or_cache_resource(
781
787
  self, key: str, default_factory: Callable[[], Any]
782
- ) -> Any:
783
- value = self._cache.get(key, self._sentinel)
784
- if value is not self._sentinel:
785
- return value
786
- # Get the value first to be sure to return the right one in case of concurrent
787
- # access.
788
- #
789
- # Additionally, the factory can be long to complete (that's one of the reason
790
- # we want to cache its results). Because we don't lock the factory call,
791
- # together with the cache update, in case of concurrent access,
792
- # quicker factories will complete first and will stay longer in the cache.
793
- # This is fine, the alternative is to lock factory + cache update to avoid
794
- # concurrent access but this will mean waiting for the first factory call to
795
- # complete, this can potentially imply longer waits than no getting the value
796
- # from the cache
797
- value = default_factory()
798
- self._cache[key] = value
799
- return value
788
+ ) -> Generator[Any, None, None]:
789
+ """Get or cache a resource, leasing it for the duration of the context.
790
+
791
+ A resource evicted while leased stays usable, its eviction callback only runs
792
+ once all its leases have been released.
793
+
794
+ :param key: resource cache key
795
+ :param default_factory: resource factory, called on cache miss
796
+ :return: the cached resource
797
+ """
798
+ value = self._get_and_acquire(key)
799
+ if value is self._sentinel:
800
+ value = default_factory()
801
+ self._cache_and_acquire(key, value)
802
+ try:
803
+ yield value
804
+ finally:
805
+ self._release(value)
800
806
 
807
+ @contextlib.asynccontextmanager
801
808
  async def async_get_or_cache_resource(
802
809
  self, key: str, default_factory: Callable[[], Awaitable[Any]]
803
- ) -> Any:
804
- value = self._cache.get(key, self._sentinel)
805
- if value is not self._sentinel:
806
- return value
807
- # Get the value first to be sure to return the right one in case of concurrent
808
- # access.
809
- #
810
- # Additionally, the factory can be long to complete (that's one of the reason
811
- # we want to cache its results). Because we don't lock the factory call,
812
- # together with the cache update, in case of concurrent access,
813
- # quicker factories will complete first and will stay longer in the cache.
814
- # This is fine, the alternative is to lock factory + cache update to avoid
815
- # concurrent access but this will mean waiting for the first factory call to
816
- # complete, this can potentially imply longer waits than no getting the value
817
- # from the cache
818
- value = await default_factory()
819
- self._cache[key] = value
810
+ ) -> AsyncGenerator[Any, None]:
811
+ """Async version of get_or_cache_resource.
812
+
813
+ :param key: resource cache key
814
+ :param default_factory: async resource factory, awaited on cache miss
815
+ :return: the cached resource
816
+ """
817
+ value = self._get_and_acquire(key)
818
+ if value is self._sentinel:
819
+ value = await default_factory()
820
+ self._cache_and_acquire(key, value)
821
+ try:
822
+ yield value
823
+ finally:
824
+ self._release(value)
825
+
826
+ def _get_and_acquire(self, key: str) -> Any:
827
+ with self._lock:
828
+ value = self._cache.get(key, self._sentinel)
829
+ if value is not self._sentinel:
830
+ self._acquire(value)
820
831
  return value
821
832
 
833
+ def _cache_and_acquire(self, key: str, value: Any) -> None:
834
+ # The factory can be long to complete (that's one of the reason we want to
835
+ # cache its results). Because we don't lock the factory call, together with
836
+ # the cache update, in case of concurrent access, quicker factories will
837
+ # complete first and will stay longer in the cache. This is fine, the
838
+ # alternative is to lock factory + cache update to avoid concurrent access but
839
+ # this will mean waiting for the first factory call to complete, this can
840
+ # potentially imply longer waits than no getting the value from the cache
841
+ with self._lock:
842
+ self._acquire(value)
843
+ # Can evict other resources, must be called without holding the lock
844
+ self._cache[key] = value
845
+
846
+ def _acquire(self, value: Any) -> None:
847
+ value_id = id(value)
848
+ self._n_leases[value_id] = self._n_leases.get(value_id, 0) + 1
849
+
850
+ def _release(self, value: Any) -> None:
851
+ value_id = id(value)
852
+ with self._lock:
853
+ self._n_leases[value_id] -= 1
854
+ if self._n_leases[value_id]:
855
+ return
856
+ del self._n_leases[value_id]
857
+ pending = self._pending_evictions.pop(value_id, None)
858
+ if pending is not None:
859
+ self._evict(*pending)
860
+
861
+ def _on_evict(self, key: Any, value: Any) -> None:
862
+ with self._lock:
863
+ if id(value) in self._n_leases:
864
+ self._pending_evictions[id(value)] = (key, value)
865
+ return
866
+ self._evict(key, value)
867
+
868
+ def _evict(self, key: Any, value: Any) -> None:
869
+ if self._eviction_callback is not None:
870
+ self._eviction_callback(key, value)
871
+
822
872
 
823
873
  def close_cm_callback(key: str, value: Any) -> None: # noqa: ARG001
824
874
  if hasattr(value, "__exit__"):
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: datashare-python
3
- Version: 0.11.2
3
+ Version: 0.11.4
4
4
  Summary: Manage Python tasks and local resources in Datashare
5
5
  Project-URL: Homepage, https://icij.github.io/datashare-python/
6
6
  Project-URL: Documentation, https://icij.github.io/datashare-python/
@@ -14,15 +14,15 @@ datashare_python/objects.py,sha256=RuDEC4dZPndMYwFKhmMlToY8jjwjupG7mGGCG5tzc6g,1
14
14
  datashare_python/task_client.py,sha256=oTmP8bvZW0UyhLNMi1AV3XIAx7hrdbxNRss2Mw2azEc,8435
15
15
  datashare_python/template.py,sha256=N1uM8TRXh5tQPV7BC1oJj-Pdm2Gs_SkjaXv50RnNYso,4682
16
16
  datashare_python/types_.py,sha256=cPcCkIk6ecILrKetPnAsrb0njHYuWJU3eZicUXy2lF8,1278
17
- datashare_python/utils.py,sha256=k85QP34hSHBRFo8YwGA7rYPO5cqCt33i3ntl2oSfHkE,28603
18
- datashare_python/worker-template.tar.gz,sha256=3Lu6zLEXlndprxXPGMN9L19XrplmxUki3v15HymMmls,236583
17
+ datashare_python/utils.py,sha256=MU_laix9TclXQ7Pt0mqqYQylYR3OKU-MItjQupcG3c8,30183
18
+ datashare_python/worker-template.tar.gz,sha256=5U1vAMg7xtJSKAz20kqz7QLknvzmREN5ddqK7RvAYyI,236093
19
19
  datashare_python/worker.py,sha256=ad36_yJOgwtj8KBheWndGkqaP_2aXTPSrYaHrbCXTvg,8389
20
20
  datashare_python/cli/__init__.py,sha256=9BPWtssDgsVfWMsZ1TtZCla0EC_kai4RHttr8oNLYOE,1401
21
21
  datashare_python/cli/project.py,sha256=w32Gy9AOL5B00uDT4in7YUCt2g68FnNbvwg2M3a8G6o,946
22
22
  datashare_python/cli/task.py,sha256=GsyR5o0fN4Pd8tJKAUgivAvCBKT3L4Ay5j_BCW1XBHk,5828
23
23
  datashare_python/cli/utils.py,sha256=p69CQb0zfixuyBkiZprhdMCc_NuYwXyAn6vC9H1UzAw,911
24
24
  datashare_python/cli/worker.py,sha256=x0p3a1_NsZPbmV7-WSVMRyW3_iadYcgyZKbDvWgymQc,5790
25
- datashare_python-0.11.2.dist-info/METADATA,sha256=STmFFjil2tcX6BgZOyEBdlraEtIs69qEWV6FGHGhGnY,1012
26
- datashare_python-0.11.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
27
- datashare_python-0.11.2.dist-info/entry_points.txt,sha256=ILE7auxabHWiu3GC-AunWnzjhOI_SbZp7D4GqZHlLw4,68
28
- datashare_python-0.11.2.dist-info/RECORD,,
25
+ datashare_python-0.11.4.dist-info/METADATA,sha256=wesn34A1Y6Omeo3gftg-Jd4jiXo4a4hktN1blM3vCJI,1012
26
+ datashare_python-0.11.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
27
+ datashare_python-0.11.4.dist-info/entry_points.txt,sha256=ILE7auxabHWiu3GC-AunWnzjhOI_SbZp7D4GqZHlLw4,68
28
+ datashare_python-0.11.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any