o
    ^v(j  ã                   @  s€   d Z ddlmZ ddlZddlmZ ddlmZmZm	Z	 ddl
mZ ddlmZ 		dddd„Z		dddd„Zdddd„ZdS )u«  TTL-aware S3 ingestion cache (QUE-223).

Registers a persistent-cache ``cloudpathlib.S3Client`` as the process-global
default for ``s3://`` URIs, so any ``CloudPath("s3://...")`` /
``AnyPath("s3://...")`` materializes through a project-local cache dir instead
of re-downloading every run. Source-change correctness is cloudpathlib's
built-in ETag check; disk hygiene is a wall-clock TTL enforced by a single
startup sweep.

No subclass, no per-access hook â€” one module, two functions. The cache dir,
TTL, and (optional) region come from ``get_settings().s3_cache`` (QUE-228);
each function still accepts explicit overrides for tests:

- ``evict_stale_s3_cache()`` â€” startup sweep. Walks the cache dir and unlinks
  any file whose access time is older than the TTL. This is the *only* TTL
  reclamation path: cloudpathlib's ETag check handles source correctness; the
  sweep handles "downloaded once, never touched again" files that would
  otherwise live in the cache forever.
- ``init_s3_cache()`` â€” package-import entrypoint. Runs the sweep, then
  constructs and registers the default S3 client. Called once from
  ``quber/__init__.py``.

TTL is keyed off ``st_atime``, so re-processing a cached document resets its
window: actively used files stay warm, abandoned ones age out after the TTL.
On ``noatime``-mounted volumes atime is frozen and TTL degrades to "time since
download" â€” accepted rather than adding mount-detection complexity.
é    )ÚannotationsN)ÚPath)ÚAnyPathÚS3ClientÚS3Path)Úlogger)Úget_settingsÚ	cache_dirúPath | NoneÚttl_secondsúfloat | NoneÚreturnÚintc                 C  s‚   t ƒ j}| dur
| n|j} |dur|n|j}|  ¡ sdS t ¡ | }d}|  d¡D ]}| ¡ r>| ¡ j	|k r>| 
¡  |d7 }q)|S )a¿  Delete cached files whose atime is older than ``ttl_seconds``.

    ``cache_dir`` / ``ttl_seconds`` default to ``get_settings().s3_cache`` when
    not passed explicitly. Walks ``cache_dir`` once with ``rglob('*')`` and
    unlinks regular files past the TTL. Returns the count evicted. Idempotent
    and safe to call when the cache directory does not exist yet (returns 0).
    Linear in the cached-file count; sub-second even at thousands.
    Nr   Ú*é   )r   Ús3_cacher	   r   ÚexistsÚtimeÚrglobÚis_fileÚstatÚst_atimeÚunlink)r	   r   Ús3ÚcutoffÚevictedÚp© r   ú//home/mande/repo/quber/src/quber/files/cache.pyÚevict_stale_s3_cache(   s   €r   ÚNonec                 C  sŒ   t ƒ j}| dur
| n|j} |dur|n|j}t| |ƒ}|r$t d|| ¡ |jr:ddl}|j	|jd�}t
| d|d�}nt
| dd�}| ¡  dS )u•  Prune the stale cache, then register the default S3 client.

    Called once at package import (see ``quber/__init__.py``). ``cache_dir`` /
    ``ttl_seconds`` default to ``get_settings().s3_cache``. Two ordered steps:

    1. ``evict_stale_s3_cache()`` â€” bound disk usage for keys not re-accessed
       between runs.
    2. Construct a persistent-cache ``S3Client`` rooted at ``cache_dir`` and
       publish it via ``set_as_default_client()`` so plain
       ``CloudPath("s3://...")`` / ``AnyPath("s3://...")`` picks it up. When
       ``s3_cache.region`` is set, the client is bound to a region-scoped boto3
       session; otherwise region resolves from boto3's own chain.

    Constructing the client requires neither AWS credentials nor network access
    (boto3 resolves both lazily on first GET), so this is safe to run at import
    on any host, including CPU-only / credential-less dev environments.
    Nz,Evicted {} stale file(s) from S3 cache at {}r   )Úregion_nameÚ
persistent)Úlocal_cache_dirÚfile_cache_modeÚboto3_session)r#   r$   )r   r   r	   r   r   r   ÚdebugÚregionÚboto3ÚSessionr   Úset_as_default_client)r	   r   r   r   r(   ÚsessionÚclientr   r   r   Úinit_s3_cacheB   s   
r-   ÚrawÚstrr   c                 C  sl   t | ƒ}t|tƒr2|dur|ntƒ jj}||j |j }| ¡ r+t	 
d||¡ t|ƒS t	 d||¡ t|ƒS )aÒ  Coerce a user-supplied path string to a local filesystem Path.

    Local inputs pass straight through. For ``s3://`` inputs, log an INFO line
    when the document is already present in the local cache (cache hit), then
    materialize it (cloudpathlib downloads on miss) and return the local Path.
    ``cache_dir`` defaults to ``get_settings().s3_cache.cache_dir``.

    The cache-hit check mirrors cloudpathlib's on-disk layout
    (``<cache_dir>/<bucket>/<key>``) using only public ``S3Path`` attributes.
    Note: cloudpathlib still revalidates the S3 ETag on access, so a logged hit
    means "present locally"; if the source object changed upstream, cloudpathlib
    transparently re-downloads the fresh copy.
    NzS3 cache hit: {} served from {}z#S3 cache miss: downloading {} to {})r   Ú
isinstancer   r   r   r	   ÚbucketÚkeyr   r   Úinfor&   r   )r.   r	   ÚdocÚcachedr   r   r   Úresolve_documentg   s   
ÿr6   )NN)r	   r
   r   r   r   r   )r	   r
   r   r   r   r    )N)r.   r/   r	   r
   r   r   )Ú__doc__Ú
__future__r   r   Úpathlibr   Úcloudpathlibr   r   r   Úlogurur   Úquber.settingsr   r   r-   r6   r   r   r   r   Ú<module>   s    þþ%