# python-ows-assets-uploader
Python library for uploading assets via ows-assets. application_family: assets

## Installation

There are two ways two install `ows-assets-uploader` as a dependency in your application.

### Install using pip (`requirements.txt`)

1. Add `-i https://pypi.theorchard.io/pypi/` to the top of `requirements.txt`.
2. `env/bin/pip install -r requirements.txt`

### Install using Poetry (`pyproject.toml`)

#### pypi.theorchard.com

1. Update `pyproject.toml` to include:

```toml
[[tool.poetry.source]]
name = "pde"
url = "https://pypi.theorchard.io/pypi/"
priority = "supplemental"
```

2. Add this to the `[tool.poetry.dependencies]` section in `pyproject.toml`:
```toml
[tool.poetry.dependencies]
...
ows-assets-uploader = {version = "^1.0.0", source = "pde"}
```

3. Run `poetry lock`

#### git+ssh

1. Add this to the `[tool.poetry.dependencies]` section in `pyproject.toml`:

```toml
[tool.poetry.dependencies]
...
ows-assets-uploader = { git = "ssh://git@github.com/theorchard/python-ows-assets-uploader"}
```

2. Run `poetry lock`

## Usage

### Quick Start

```python
import ows_assets_uploader

# Set environment (required)
ows_assets_uploader.set_env("qa")  # or "prod"

# Upload product artwork from a local file
destination_filename = ows_assets_uploader.upload(
    source_file_location="path/to/image.tif",
    asset_upload_type="static_artwork",
    product_id=123456,
)

# Upload track audio from a local file
destination_filename = ows_assets_uploader.upload(
    source_file_location="path/to/audio.wav",
    asset_upload_type="stereo",
    product_id=123456,
    track_id=234567,
)

# Upload product artwork from S3
destination_filename = ows_assets_uploader.upload(
    source_file_location="s3://bucket/path/to/image.tif",
    asset_upload_type="static_artwork",
    product_id=123456,
)

# Upload product artwork from http URL
destination_filename = ows_assets_uploader.upload(
    source_file_location="https://example.com/path/to/image",
    asset_upload_type="static_artwork",
    product_id=123456,
)

# Upload with explicit source_filename
destination_filename = ows_assets_uploader.upload(
    source_file_location="path/to/f90dc920-dcd3-4a4b-a848-46e3bd472d91",
    asset_upload_type="static_artwork",
    product_id=123456,
    source_filename="custom_filename.tif",
)

# Upload spatial track audio
destination_filename = ows_assets_uploader.upload(
    source_file_location="path/to/audio.wav",
    asset_upload_type="atmos",
    product_id=123456,
    track_id=234567,
)

# Upload with a custom OwsClient instance
from owsclient import OwsClient

my_client = OwsClient(environment="prod", service_name="my-service", retries=3)
destination_filename = ows_assets_uploader.upload(
    source_file_location="path/to/audio.wav",
    asset_upload_type="stereo",
    product_id=123456,
    ows_client=my_client,
)
```

### Supported File Sources

- **Local files**: Any file accessible by the local filesystem
- **S3 objects**: Files in S3 buckets using `s3://bucket/key` URLs
- **HTTP/HTTPS URLs**: Files accessible via HTTP or HTTPS
- **Other filesystems**: Any source supported by `fsspec`

### Source Filename Detection

The library automatically determines the source filename:

1. **Explicit override**: If you provide `source_filename` parameter
2. **Extracted from path**: Uses the basename of the file path
3. **HTTP/HTTPS URLs**: Extracts filename from the URL path component (ignoring query parameters)
4. **Truncation**: Filenames exceeding 64 characters are truncated in the middle (e.g., `very...name.wav`)

### Bringing Your Own OwsClient

By default, the library creates an `OwsClient` instance using the environment set via `set_env()`. If you need to control how the client is configured (e.g. to share an existing client, customise retries, or inject one in tests), you can pass your own instance via the `ows_client` parameter:

```python
from owsclient import OwsClient
import ows_assets_uploader

my_client = OwsClient(environment="prod", service_name="my-service", retries=3)
destination_filename = ows_assets_uploader.upload(
    source_file_location="path/to/audio.wav",
    asset_upload_type="stereo",
    product_id=123456,
    ows_client=my_client,
)
```

When `ows_client` is provided, `set_env()` is not required.

### M2M (JWT) Authentication

To have the default client authenticate with a machine-to-machine JWT, register an
`M2MTokenManager` via `set_m2m_token_manager()` before uploading.
The library builds the underlying `OwsClient` for you and
the token manager attaches an `Authorization: Bearer <token>` header to every request.

```python
from owsclient import M2MTokenManager
import ows_assets_uploader

ows_assets_uploader.set_env("prod")
ows_assets_uploader.set_m2m_token_manager(
    M2MTokenManager(
        secrets_manager=my_secrets_manager,
        environment="prod",
        service_name="my-service",
    )
)

destination_filename = ows_assets_uploader.upload(
    source_file_location="path/to/audio.wav",
    asset_upload_type="stereo",
    product_id=123456,
)
```

`set_m2m_token_manager()` is ignored when you pass your own `ows_client` (that client
carries its own authentication). To use M2M auth with `AssetsUploader` directly,
construct an `OwsClient(..., m2m_token_manager=...)` and pass it as `ows_client`.

### Using `AssetsUploader` Directly

For more control — for example, reusing a client across multiple uploads — use the `AssetsUploader` class directly:

```python
from owsclient import OwsClient
from ows_assets_uploader import AssetsUploader

ows_client = OwsClient(environment="prod", service_name="my-service", retries=3)
uploader = AssetsUploader(ows_client=ows_client)

destination_filename = uploader.upload(
    source_file_location="path/to/audio.wav",
    asset_upload_type="stereo",
    product_id=123456,
    track_id=234567,
)
```

### OBO (On-Behalf-Of) Uploads with `ImpersonationOwsClient`

To upload on behalf of another identity, pass an `ImpersonationOwsClient` and the target `impersonated_identity_uuid` to `AssetsUploader`:

```python
from owsclient import ImpersonationM2MTokenManager, ImpersonationOwsClient
from ows_assets_uploader import AssetsUploader

obo_client = ImpersonationOwsClient(
    environment="prod",
    service_name="my-service",
    m2m_token_manager=ImpersonationM2MTokenManager(
        secrets_manager=my_secrets_manager,
        environment="prod",
        service_name="my-service",
    ),
)
uploader = AssetsUploader(
    ows_client=obo_client,
    impersonated_identity_uuid="<identity-uuid>",
)

destination_filename = uploader.upload(
    source_file_location="path/to/audio.wav",
    asset_upload_type="stereo",
    product_id=123456,
    track_id=234567,
)
```

`impersonated_identity_uuid` is fixed at construction time, so one `AssetsUploader` instance acts on behalf of exactly one identity. Construct a new instance of `AssetsUploader` per identity as needed.

Passing `impersonated_identity_uuid` without an `ImpersonationOwsClient` (or vice versa) raises a `ValueError` at construction time.

## Contributing

### Dependencies

This library uses Poetry for dependency management. Please use `poetry add` when adding new dependencies. Always add/commit changes made to the `pyproject.toml` and `poetry.lock` files.

Avoid being overly-restrictive when adding installation requirements. [This](https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/) has a good overview of considerations when specifying what is required to install ows-assets-uploader. A few key quotes:

> It's best practice to indicate any known lower or upper bounds
> It is not considered best practice to use install_requires to pin dependencies to specific versions, or to specify sub-dependencies (i.e. dependencies of your dependencies). This is overly-restrictive, and prevents the user from gaining the benefit of dependency upgrades.

Do indicate if a dependency belongs to development (and thus, not required when the library is installed) using `poetry add <the new dependency> --group dev`.

On a regular basis, run `poetry update` to update to the latest versions of all dependencies in the `poetry.lock` file. Specific dependencies can be updated with `poetry update <the specific dependencies>`.

### `make` targets

You can run `make help` to view all the documented targets for working in this library.

### Packaging/Versioning

Use semver and https://pipeline.theorchard.io/job/theorchard/job/python-ows-assets-uploader/job/master/build to build/publish new versions of this library to the private PyPI repository at https://pypi.theorchard.io.

After a release tag has been pushed to Github, use the Github UI to Create a Release from the tag. Start from the autogenerated notes, and add salient details to the change(s) being added. This will be used by clients of the library to determine what to expect when upgrading (easy vs breaking?).
