Metadata-Version: 2.4
Name: okeanoslabs-bloom-feedback
Version: 0.1.2
Summary: Server-side client for sending application feedback to the Bloom workspace intake API.
Project-URL: Homepage, https://github.com/OkeanosLabs/bloom-integrations
Project-URL: Source, https://github.com/OkeanosLabs/bloom-integrations/tree/main/python-package
Author: Okeanos Labs
License: MIT License
        
        Copyright (c) 2026 Okeanos Labs
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: bloom,bug-reporting,error-reporting,feedback,okeanoslabs
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Bug Tracking
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# okeanoslabs-bloom-feedback

Framework-neutral Python client for submitting application feedback to Bloom's workspace intake API. It has no UI, authentication, routing, or framework dependencies, and no runtime dependencies at all.

This is the Python counterpart of [`@okeanoslabs/bloom-feedback`](../npm-package). Both clients:

- send `Authorization: Bearer <api_key>` requests to Bloom;
- resolve a stable `ship_tag` to the newest active voyage and cache that lookup for ten minutes by default; or use a fixed `voyage_id`;
- map application feedback to `POST /intake/errors`;
- add trusted reporter identity, page context, and the last few application actions to the description; and
- raise structured errors suitable for a server route or API handler.

They also produce **byte-identical request bodies** for the same input, which `tests/test_parity.py` verifies against output generated by the JavaScript package.

## Install

```sh
pip install okeanoslabs-bloom-feedback
```

Requires Python 3.10 or later.

## Use from server code

Do not import this client into anything that runs on an end user's machine. Its API key must stay in a server-only environment variable or secret store.

```python
import os

from bloom_feedback import BloomFeedbackClient, BloomFeedbackError, FeedbackReporter

bloom = BloomFeedbackClient(
    api_key=os.environ["BLOOM_API_KEY"],
    ship_tag=os.environ.get("BLOOM_SHIP_TAG", "my-service"),
    base_url=os.environ.get("BLOOM_BASE_URL"),
)


def submit_feedback(authenticated_user, body):
    try:
        return bloom.submit(
            title=body["title"],
            description=body.get("description"),
            priority=body.get("priority"),
            page_url="/settings/profile",
            page_title="Profile settings",
            reporter=FeedbackReporter(name=authenticated_user.name, email=authenticated_user.email),
            actions=[
                {"type": "navigation", "label": "/settings/profile"},
                {"type": "notification", "level": "error", "label": "Profile update failed"},
            ],
        )
    except BloomFeedbackError as error:
        # Map error.code / error.status to your framework's response shape.
        raise
```

Pass `voyage_id` instead of `ship_tag` when a caller must always file feedback to one known voyage. A fixed voyage takes precedence when both options are present.

### asyncio

`AsyncBloomFeedbackClient` takes the same options and has the same methods as coroutines. Its default transport runs the standard-library request in a worker thread, so it never blocks the event loop:

```python
from bloom_feedback import AsyncBloomFeedbackClient

bloom = AsyncBloomFeedbackClient(api_key=..., ship_tag="my-service")
result = await bloom.submit(title="Cannot save profile settings", priority="high")
```

### Calling styles

`submit()` accepts keyword fields, a `FeedbackSubmission`, or a mapping. Mappings also accept the JavaScript package's `pageUrl` / `pageTitle` / `userAgent` spellings, so a validated browser request body can be forwarded without renaming:

```python
bloom.submit(title="Report", page_url="/x")                     # keyword fields
bloom.submit(FeedbackSubmission(title="Report", page_url="/x"))  # dataclass
bloom.submit({"title": "Report", "pageUrl": "/x"})               # mapping, camelCase accepted
```

## Typical integration

Keep a browser-facing feedback form separate from the server-side Bloom client:

```text
Feedback form -> your server handler -> BloomFeedbackClient
              -> Bloom POST /intake/errors
```

The form can gather a title, description, priority, category, page context, browser user agent, and recent application actions. The server owns the Bloom API key and should obtain the reporter from its authenticated identity; it must not trust reporter identity supplied by the browser.

Construct the client with `BLOOM_API_KEY`, `BLOOM_BASE_URL`, and either `BLOOM_SHIP_TAG` or `BLOOM_VOYAGE_ID`, then call `submit()` after validating the authenticated request.

## API

`BloomFeedbackClient(**options)` and `AsyncBloomFeedbackClient(**options)` accept keyword arguments only:

| Option | Required | Description |
| --- | --- | --- |
| `api_key` | Yes | Write-capable Bloom organization API key. |
| `ship_tag` | One target required | Stable service tag; the active voyage is resolved automatically. |
| `voyage_id` | One target required | Fixed voyage ID; skips lookup and overrides `ship_tag`. |
| `base_url` | No | Defaults to `https://bloom-workspace-api.okeanoslabs.com`. |
| `voyage_cache_ttl` | No | Active-voyage cache duration **in seconds**; default is 600. Set `0` to disable. |
| `timeout` | No | Per-request timeout in seconds; default is 10. |
| `transport` | No | Custom HTTP implementation for tracing, proxies, pooling, or tests. |

Note the unit change from the JavaScript package: `voyage_cache_ttl` is seconds, not milliseconds.

Methods:

- `submit(submission=None, **fields)` — send a report; returns Bloom's intake response as a `dict`. The submission is validated before any network call, so a bad title never costs a request.
- `resolve_voyage_id()` — preflight the configured target.
- `clear_voyage_cache()` — force a new lookup after a voyage change.
- `build_payload(voyage_id, submission=None, **fields)` — build the exact body that would be sent, without sending it.

Module-level helpers `build_feedback_description(...)` and `build_bloom_intake_payload(voyage_id, ...)` are pure and let an application preview what will be sent.

Instances are safe to share across threads (and across tasks, for the async client). A concurrent first lookup is collapsed into a single request rather than a stampede.

### Custom transports

`transport` receives a `BloomRequest` and returns a `BloomResponse`. A non-2xx status must be returned, not raised; anything the callable raises becomes a `network_error`.

```python
import httpx
from bloom_feedback import BloomFeedbackClient, BloomResponse

session = httpx.Client(timeout=10.0)

def httpx_transport(request):
    reply = session.request(request.method, request.url, headers=dict(request.headers), content=request.body)
    return BloomResponse(status=reply.status_code, body=reply.content)

bloom = BloomFeedbackClient(api_key=..., ship_tag="my-service", transport=httpx_transport)
```

### Errors

Every failure is a `BloomFeedbackError` with a stable `code`, an optional upstream HTTP `status`, and at most 500 characters of `response_body`. Codes are `invalid_configuration`, `invalid_input`, `network_error`, `unknown_ship`, `invalid_ship_response`, `ship_lookup_failed`, `voyage_lookup_failed`, `no_active_voyage`, `intake_rejected`, and `invalid_intake_response`; the full tuple is exported as `ERROR_CODES`.

## Development

```sh
cd python-package
PYTHONPATH=src python -m unittest discover -s tests -v
```

The suite has no third-party dependencies; `pytest` also runs it unchanged. `tests/test_transport.py` starts a loopback HTTP server to exercise the default `urllib` transport.

After a deliberate change to the payload rules in either package, regenerate the cross-language fixture and update both clients together:

```sh
npm --prefix ../npm-package run build
node tests/parity_from_js.mjs ../npm-package/dist/index.js tests/parity_fixtures.json tests/parity_expected.jsonl
```

## Publishing

```sh
python -m pip install build twine
python -m build
python -m twine upload dist/*
```

The distribution name is `okeanoslabs-bloom-feedback` and the import name is `bloom_feedback`. Confirm the name is available on PyPI, or change `name` in `pyproject.toml`, before the first upload.
