Metadata-Version: 2.4
Name: ppx-connectors
Version: 0.1.0a4
Summary: Outbound ingestion connectors for the Preference Profile Exchange (PPX). Pulls preference signals from third-party services (Spotify, Strava, Google Calendar, …) and writes claims to a PPX provider via propose_updates.
Project-URL: Homepage, https://ppx.dev/
Author: Blazing Customs
License: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27.0
Requires-Dist: ppx-client>=0.1.0a4
Requires-Dist: pydantic>=2.9.0
Provides-Extra: google
Provides-Extra: spotify
Provides-Extra: strava
Description-Content-Type: text/markdown

# ppx-connectors

> Outbound ingestion connectors for the [Preference Profile Exchange (PPX)](https://blazing-customs.github.io/ppx-spec/).

Pull preference signals from services the user already uses, map them
onto PPX claims, and propose updates through the provider — so writeback
policy, review, and audit all apply uniformly.

## Install

```bash
pip install ppx-connectors
```

Requires Python 3.11+. Published version: **0.1.0a1** ([PyPI](https://pypi.org/project/ppx-connectors/)).

> Alpha, tracking a **draft** specification. Expect breaking changes.
> The version is `0.1.0a1` on PyPI and `0.1.0-alpha.1` on npm — the
> same release in each ecosystem's required format.

## Ship your own connector in three methods

```python
from ppx_connectors import Connector, ConnectorContext, ProposedClaim, SourceSignal

class MyConnector(Connector):
    name = "my-source"
    writes_namespaces = ["my-domain"]
    interval_seconds = 60 * 60 * 12  # every 12h

    def fetch(self, ctx: ConnectorContext) -> list[SourceSignal]:
        # call the external API, return raw signals
        ...

    def to_claims(self, signals: list[SourceSignal]) -> list[ProposedClaim]:
        # map raw signals onto spec claims (pure function, no I/O)
        return [ProposedClaim(
            namespace="my-domain",
            key="some_preference",
            value=0.7,
            confidence=0.8,
            source={"kind": "imported", "origin": self.name},
        )]
```

## Run a connector once (manual / cron-friendly)

```python
from ppx_client import PpxClient
from ppx_connectors import ConnectorContext, run_once
from ppx_connectors.connectors import SpotifyConnector

client = PpxClient("https://api.provider.app")
ctx = ConnectorContext(
    client=client,
    grant_token=writeback_scoped_grant_token,
    credentials={"spotify_access_token": user_spotify_oauth_token},
)
result = run_once(SpotifyConnector(), ctx)
# → {"connector": "spotify", "signals": 150, "proposed": 6, "writeback": "proposal_created"}
```

## Built-in connectors

| Name | Namespace | Claim keys | Status |
| --- | --- | --- | --- |
| `SpotifyConnector` | `music` | `genre_preference`, `energy_preference`, `tempo_preference`, `valence_preference`, `danceability_preference`, `acousticness_preference`, `listening_intensity` | shipping |
| `StravaConnector` | `fitness` | `activity_frequency`, `terrain_preference`, `cardio_load`, `pace_preference`, `outdoor_bias` | shipping |
| `GoogleCalendarConnector` | `core` | `social_energy_preference`, `pace_preference`, `morning_evening_skew`, `weekend_activity` | shipping |

## Design notes

- **Separation of concerns.** `fetch()` does I/O; `to_claims()` is pure.
  This makes claim-mapping testable without hitting external services,
  and reusable for replay / dry-run.
- **Spec-compliant writeback.** All claims go through `/v1/profile/propose-updates`,
  so the user's `writeback_policy` is honored automatically. Under
  `review_required` the user sees every proposal before anything takes effect.
- **Confidence by volume.** Pull-based inference has an inherent signal
  count; the built-in `_conf_from_volume` helper scales confidence
  linearly with observation count.
- **Every claim is attributed.** `source.kind = "imported"` and
  `source.origin = <connector-name>` on every proposal. The user always
  knows *who* learned each claim.

## License

Apache-2.0.
