Metadata-Version: 2.5
Name: phactor
Version: 0.2.0
Summary: Python SDK for the Phactor clinical trial feasibility platform
Project-URL: Homepage, https://phactor.ai
Project-URL: Documentation, https://docs.phactor.ai
Author: Janeiro Digital
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: clinical-trials,cohort,feasibility,graphql,healthcare,real-world-data,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Healthcare Industry
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
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: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.0
Requires-Dist: pyjwt<3,>=2.0
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pandas-stubs>=2.0; extra == 'dev'
Requires-Dist: pandas>=1.5; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.22; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Provides-Extra: pandas
Requires-Dist: pandas>=1.5; extra == 'pandas'
Description-Content-Type: text/markdown

# Phactor Python SDK

Python SDK for the Phactor clinical trial feasibility platform. Authenticate via OAuth2 client credentials and run cohort feasibility queries programmatically.

## Installation

```bash
pip install phactor
```

## Quick Start

### Async

```python
import asyncio

from phactor import AsyncPhactorClient


async def main() -> None:
    async with AsyncPhactorClient(verify="/etc/ssl/certs/zscaler-ca.pem") as client:
        result = await client.cohorts.analyze(
            providers=["provider-uuid"],
            cohort_groups=[
                {
                    "inclusionGroupOperator": "AND",
                    "inclusionGroups": [
                        {
                            "name": "Adults",
                            "operator": "AND",
                            "propositions": [{"age": {"from": 18, "to": 65}}],
                        }
                    ],
                }
            ],
            include_impact_analysis=True,
        )

        for analysis in result.analyses:
            print(f"{analysis.provider.name}: {analysis.total_patients} patients")


asyncio.run(main())
```

### Sync

```python
from phactor import PhactorClient

with PhactorClient(verify="/etc/ssl/certs/zscaler-ca.pem") as client:
    result = client.cohorts.analyze(
        providers=["provider-uuid"],
        cohort_groups=[
            {
                "inclusionGroupOperator": "AND",
                "inclusionGroups": [
                    {
                        "name": "Adults",
                        "operator": "AND",
                        "propositions": [{"age": {"from": 18, "to": 65}}],
                    }
                ],
            }
        ],
    )

    for analysis in result.analyses:
        print(f"{analysis.provider.name}: {analysis.total_patients} patients")
```

## Configuration

Pass credentials directly or use environment variables:

| Parameter | Environment Variable |
|-----------|---------------------|
| `client_id` | `PHACTOR_CLIENT_ID` |
| `client_secret` | `PHACTOR_CLIENT_SECRET` |
| `entity_id` | `PHACTOR_ENTITY_ID` |
| `fusionauth_url` | `PHACTOR_FUSIONAUTH_URL` |
| `gateway_url` | `PHACTOR_GATEWAY_URL` |
| `verify` | `PHACTOR_CA_BUNDLE` |
| `timeout` | `PHACTOR_TIMEOUT` |

```bash
export PHACTOR_CLIENT_ID="your-client-id"
export PHACTOR_CLIENT_SECRET="your-client-secret"
export PHACTOR_ENTITY_ID="your-entity-id"
export PHACTOR_FUSIONAUTH_URL="https://auth.phactor.ai"
export PHACTOR_GATEWAY_URL="https://federated-gateway.phactor.ai/graphql"
export PHACTOR_CA_BUNDLE="/etc/ssl/certs/zscaler-ca.pem"
export PHACTOR_TIMEOUT="600"   # seconds; optional, this is the default
```

If you prefer explicit configuration, pass the same values into `PhactorClient(...)` or `AsyncPhactorClient(...)`.

If `verify` is not passed explicitly, the SDK will use `PHACTOR_CA_BUNDLE` when present and otherwise default to standard TLS verification. The same setting is used for both FusionAuth token requests and Phactor gateway API requests.

```python
from phactor import PhactorClient

client = PhactorClient(
    client_id="your-client-id",
    client_secret="your-client-secret",
    fusionauth_url="https://auth.phactor.ai",
    gateway_url="https://federated-gateway.phactor.ai/graphql",
    verify="/etc/ssl/certs/zscaler-ca.pem",
)
```

## Reliability Notes

- The SDK caches OAuth tokens and refreshes them automatically when needed.
- API requests retry on transient failures such as `429`, `502`, `503`, `504`, timeouts, and temporary connection errors.
- The request timeout defaults to **600 seconds**, deliberately above the platform's own deadlines (connector 420s < query fan-out 540s), so a slow cohort returns a server error naming the provider rather than a client-side `TimeoutError` that is then retried three times. Lower it with `PHACTOR_TIMEOUT` or `timeout=` if you want to fail faster.
- If an API request returns `401`, the SDK invalidates the cached token, fetches a fresh token, and retries once before raising `AuthenticationError`.
- `AsyncPhactorClient` should be used through `async with` or closed explicitly with `await client.close()`. Once closed, further use raises `PhactorError`.

## Fluent Cohort Builder

```python
from phactor.cohorts import CohortBuilder, Proposition

cohort = (
    CohortBuilder(inclusion_operator="OR")
    .include("Core criteria", operator="AND")
    .add(Proposition(age={"from": 18}))
    .add(
        Proposition(
            conditions={
                "values": [
                    {"type": "ICD10", "values": ["E11"]}  # Type 2 Diabetes
                ]
            }
        )
    )
    .done()
    .exclude("Recent CV events", operator="OR")
    .add(
        Proposition(
            conditions={
                "values": [
                    {
                        "type": "ICD10",
                        "values": ["I21"],  # myocardial infarction
                        "timing": {
                            "operator": "LESS_THAN_OR_EQUALS",
                            "value": 180,
                            "unit": "DAYS",
                            "reference": "SCREENING",
                        },
                    }
                ]
            }
        )
    )
    .done()
    .build()
)

async with AsyncPhactorClient() as client:
    result = await client.cohorts.analyze(
        providers=["provider-1"],
        cohort_groups=[cohort],
        include_impact_analysis=True,
    )
```

You can also pass typed models directly instead of raw dictionaries:

```python
from phactor import CohortGroupInput, CriteriaGroupInput, PropositionInput

cohort = CohortGroupInput(
    inclusion_group_operator="AND",
    inclusion_groups=[
        CriteriaGroupInput(
            name="Adults",
            operator="AND",
            propositions=[PropositionInput(age={"from": 18, "to": 65})],
        )
    ],
)
```

There are three independent operator levels:

- `cohort_group_operator` combines complete pathways when a request intentionally
  contains more than one `CohortGroupInput`.
- `inclusion_group_operator` combines the inclusion groups inside one pathway.
- `CriteriaGroupInput.operator` (`AND`, `OR`, or `SOME`) combines propositions
  inside one named criteria group.

Exclusion groups are always unioned and subtracted from the pathway's combined
inclusion result. For example, an `OR` pathway with inclusion groups `A` and `B`
and exclusion group `E` evaluates as `(A OR B) EXCEPT E`.
