Skip to main content

Resource discovery and resolution library for Python

Project description

JustMyResource

A precise, lightweight, and extensible resource discovery library for Python. JustMyResource provides a robust "Resource Atlas" for the Python ecosystem—a definitive map of every resource available to an application, whether bundled in a Python package or provided by third-party resource packs.

Features

  • Generic Framework: Unified interface for multiple resource types (SVG icons, raster images, future: audio/video)
  • Extensible: Resource packs can be added via standard Python EntryPoints mechanism
  • Efficient: Lazy discovery with in-memory caching
  • Prefix-Based Resolution: Namespace disambiguation via pack:name format
  • Type-Safe: Returns ResourceContent objects with MIME types and metadata
  • Zero Dependencies: Core library has no required dependencies

Installation

pip install justmyresource

Quick Start

from justmyresource import ResourceRegistry, get_default_registry

# Get default registry
registry = get_default_registry()

# Get resource with fully qualified name (always unique)
content = registry.get_resource("acme-icons/lucide:lightbulb")

# Get resource with short pack name (works if unique)
content = registry.get_resource("lucide:lightbulb")

# Get resource with alias
content = registry.get_resource("luc:lightbulb")

# Check content type and use accordingly
if content.content_type == "image/svg+xml":
    svg_text = content.text  # Decode as UTF-8
    # Use SVG text...

# Get resource without prefix (searches by priority)
content = registry.get_resource("logo")

Basic Usage

Getting Resources

from justmyresource import ResourceRegistry

registry = ResourceRegistry()

# Get resource with fully qualified name (always unique)
content = registry.get_resource("acme-icons/lucide:lightbulb")

# Get resource with short pack name (works if unique)
content = registry.get_resource("lucide:lightbulb")

# Get resource without prefix (searches packs by priority)
content = registry.get_resource("logo")

# Access resource data
if content.content_type == "image/svg+xml":
    svg_text = content.text  # UTF-8 decoded string
elif content.content_type == "image/png":
    png_bytes = content.data  # Raw bytes

Listing Resources

# List all resources from all packs
for resource_info in registry.list_resources():
    print(f"{resource_info.pack}:{resource_info.name} ({resource_info.content_type})")
    # pack is qualified name (e.g., "acme-icons/lucide")

# List resources from a specific pack (qualified or short name)
for resource_info in registry.list_resources(pack="acme-icons/lucide"):
    print(resource_info.name)
for resource_info in registry.list_resources(pack="lucide"):
    print(resource_info.name)

# List registered packs (returns qualified names)
for qualified_name in registry.list_packs():
    print(qualified_name)  # e.g., "acme-icons/lucide"

Blocking Resource Packs

# Block specific packs (accepts short or qualified names)
registry = ResourceRegistry(blocklist={"broken-pack", "acme-icons/lucide"})

# Block via environment variable
# RESOURCE_DISCOVERY_BLOCKLIST="broken-pack,acme-icons/lucide" python app.py

Handling Prefix Collisions

When multiple packs claim the same prefix, the registry emits warnings and resolves by priority:

import warnings

# Filter collision warnings if desired
warnings.filterwarnings("ignore", category=PrefixCollisionWarning)

registry = ResourceRegistry()

# Both packs remain accessible via qualified names
content1 = registry.get_resource("acme-icons/lucide:lightbulb")
content2 = registry.get_resource("cool-icons/lucide:lightbulb")

# Inspect collisions
collisions = registry.get_prefix_collisions()
# {"lucide": ["acme-icons/lucide", "cool-icons/lucide"]}

Custom Prefix Mapping

Override prefix mappings to resolve collisions or add custom aliases:

# Via constructor
registry = ResourceRegistry(
    prefix_map={
        "icons": "acme-icons/lucide",  # Map "icons" to specific pack
        "mi": "material-icons/core",   # Custom alias
    }
)

# Via environment variable
# RESOURCE_PREFIX_MAP="icons=acme-icons/lucide,mi=material-icons/core" python app.py

# Inspect current mappings
prefix_map = registry.get_prefix_map()
# {"icons": "acme-icons/lucide", "mi": "material-icons/core", ...}

Creating Resource Packs

Resource packs can be registered via Python EntryPoints. This allows applications to bundle resources or third-party packages to provide resources.

First-Party Resource Pack (Application's Own Resources)

# myapp/resources.py
from collections.abc import Iterator
from pathlib import Path
from importlib.resources import files
from justmyresource.types import ResourceContent, ResourcePack

class MyAppResourcePack:
    """Resource pack for application's bundled resources."""
    
    def __init__(self):
        package = files("myapp.resources")
        self._base_path = Path(str(package))
    
    def get_resource(self, name: str) -> ResourceContent:
        """Get resource from bundled files."""
        # Try SVG first
        svg_path = self._base_path / f"{name}.svg"
        if svg_path.exists():
            with open(svg_path, "rb") as f:
                return ResourceContent(
                    data=f.read(),
                    content_type="image/svg+xml",
                    encoding="utf-8",
                )
        
        # Try PNG
        png_path = self._base_path / f"{name}.png"
        if png_path.exists():
            with open(png_path, "rb") as f:
                return ResourceContent(
                    data=f.read(),
                    content_type="image/png",
                )
        
        raise ValueError(f"Resource not found: {name}")
    
    def list_resources(self) -> Iterator[str]:
        """List all resources."""
        for path in self._base_path.iterdir():
            if path.suffix in (".svg", ".png"):
                yield path.stem
    
    def get_priority(self) -> int:
        return 100
    
    def get_prefixes(self) -> list[str]:
        return ["myapp"]  # Optional aliases (pack name is auto-registered)

# myapp/__init__.py
def get_resource_provider():
    """Entry point factory for application's bundled resources."""
    from myapp.resources import MyAppResourcePack
    return MyAppResourcePack()
# pyproject.toml
[project.entry-points."justmyresource.packs"]
"myapp-resources" = "myapp:get_resource_provider"

Third-Party Resource Pack

# my_resource_pack/__init__.py
from my_resource_pack.provider import MyResourcePack

def get_resource_provider():
    """Entry point factory for resource pack."""
    return MyResourcePack()

Architecture

JustMyResource follows a unified "Resource Pack" architecture where all resource sources implement the same ResourcePack protocol. This ensures:

  • Consistency: All resources are discovered and resolved the same way
  • Extensibility: New resource sources can be added via EntryPoints
  • Priority: Resource packs (priority 100) override system resources (priority 0, future)

See docs/architecture.md for detailed architecture documentation.

ResourceContent Type

Resources are returned as ResourceContent objects:

@dataclass(frozen=True, slots=True)
class ResourceContent:
    data: bytes                    # Raw resource bytes
    content_type: str              # MIME type: "image/svg+xml", "image/png", etc.
    encoding: str | None = None    # Encoding for text resources (e.g., "utf-8")
    metadata: dict[str, Any] | None = None  # Optional pack-specific metadata
    
    @property
    def text(self) -> str:
        """Decode data as text (raises if encoding is None)."""
        ...

This wrapper allows consumers to:

  • Branch on content_type to handle different resource types
  • Access text content via .text property for text-based resources
  • Access pack-specific metadata via .metadata dict
  • Handle mixed-format packs (e.g., a samples pack with both SVG and PNG)

Development

Setup

# Clone the repository
git clone https://github.com/kws/justmyresource.git
cd justmyresource

# Install with development dependencies
pip install -e ".[dev]"

Running Tests

# Run tests with coverage
pytest

# Run with coverage report
pytest --cov=justmyresource --cov-report=html

Code Quality

# Format code
ruff format .

# Lint code
ruff check .

# Type checking
mypy src/

Requirements

  • Python 3.10+
  • No required dependencies (core library)
  • Resource packs may have their own dependencies

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please read the architecture documentation in docs/architecture.md and follow the project philosophy outlined in AGENTS.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

justmyresource-0.1.0.tar.gz (13.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

justmyresource-0.1.0-py3-none-any.whl (12.5 kB view details)

Uploaded Python 3

File details

Details for the file justmyresource-0.1.0.tar.gz.

File metadata

  • Download URL: justmyresource-0.1.0.tar.gz
  • Upload date:
  • Size: 13.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.2 CPython/3.10.6 Darwin/24.6.0

File hashes

Hashes for justmyresource-0.1.0.tar.gz
Algorithm Hash digest
SHA256 cee770ce7042e67104f2f7e029208e5b68ddf67cb8ea5f7bbe699704d346b04d
MD5 6f02b0136a60fa89678350c46338aa01
BLAKE2b-256 3636a28116991ee5ba0dca93458c2219771295eaaaa6992e2346b3249356d304

See more details on using hashes here.

File details

Details for the file justmyresource-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: justmyresource-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 12.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.2 CPython/3.10.6 Darwin/24.6.0

File hashes

Hashes for justmyresource-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 853864d194d1de1f280df5d2e05afc380630c24681c012343ac2aeb9c4c286a7
MD5 8c2101a7b2202c67c3873ef253e06b5e
BLAKE2b-256 cd70325bc7436135b6259511d780790fedb098c132c03fb3a353433755e86b0d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page