Metadata-Version: 2.4
Name: syncari-sdk
Version: 1.3.23
Summary: Syncari Synapse Development Kit
Author: Syncari
Author-email: dev@syncari.com
License: TBD
Requires-Python: >=3.7.0
Description-Content-Type: text/markdown
Requires-Dist: pydantic~=1.6
Requires-Dist: requests
Requires-Dist: urllib3==1.26.0
Requires-Dist: backoff
Requires-Dist: google-cloud-logging==3.10.0
Dynamic: author
Dynamic: author-email
Dynamic: description
Dynamic: description-content-type
Dynamic: license
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# Syncari Python SDK

Syncari Python Synapse Development Kit or `synapse-sdk`

## Helpers

`syncari.helpers` provides utilities for the common edge cases a synapse hits when
mapping an external system onto Syncari records: epoch-millisecond dates, fabricated
modified dates, flat-vs-nested payloads, sending every field, and CSV data.

All helpers are pure functions importable from `syncari.helpers`:

```python
from syncari.helpers import (
    to_epoch_millis, from_epoch_millis, now_epoch_millis,
    seconds_to_millis, millis_to_seconds, fabricate_last_modified,
    flatten, unflatten, get_in,
    all_field_names, with_all_fields, build_record, build_record_from_connection,
    csv_to_records, records_to_csv,
)
```

### Dates and epoch millis

Syncari stores `Record.lastModified` and `Record.createdAt` as epoch milliseconds (UTC).
These helpers convert to and from that representation.

```python
to_epoch_millis('2021-01-01T00:00:00Z')          # 1609459200000
to_epoch_millis(datetime(2021, 1, 1))            # naive datetime is treated as UTC
from_epoch_millis(1609459200000)                 # datetime(2021, 1, 1, tzinfo=utc)
now_epoch_millis()                               # current time as epoch millis
seconds_to_millis(1609459200)                    # 1609459200000
```

`to_epoch_millis` accepts a `datetime`, a `date`, or an ISO-8601 string (a trailing `Z` is
accepted). It rejects integers on purpose, so that seconds are never silently mistaken for
millis; use `seconds_to_millis` for epoch seconds.

For systems that do not expose a real modified date, fabricate one. Only fields that are
`None` are filled, unless `force=True`:

```python
fabricate_last_modified(records)                       # set lastModified to now where missing
fabricate_last_modified(records, when=1609459200000)   # use a specific stamp
fabricate_last_modified(record, set_created=True)      # also fill createdAt
```

Accepts a single `Record` or a list and returns the same shape.

### Flatten and unflatten

For APIs that reject flat schemas (or, conversely, need a flat payload), repackage the
dictionary. Keys are joined with a separator (`.` by default).

```python
flatten({'a': {'b': {'c': 1}}})       # {'a.b.c': 1}
unflatten({'a.b.c': 1})               # {'a': {'b': {'c': 1}}}
flatten(nested, sep='/')              # custom separator
```

`unflatten` raises `ValueError` when a key would be both a leaf and a branch
(for example `{'a': 1, 'a.b': 2}`).

Limitations: only nested dicts are descended into (no list indices), and round-tripping is
not safe when keys themselves contain the separator. Integer keys become strings.

To pluck a *single* nested value into a flat field (rather than flattening the whole dict),
use `get_in`. It returns `default` when any step along the path is missing, so it never
raises on absent keys:

```python
row['value_amount']   = get_in(row, 'value.amount')             # 100  -> flat key
row['value_currency'] = get_in(row, 'value.currency', default='USD')
owner_id              = get_in(row, ['owner_id', 'id'])         # list path also accepted
```

This fits connectors that return nested reference objects (e.g. `{'owner_id': {'id': 42}}`)
where you want one inner value, not the whole subtree flattened.

### Sending all fields

Build a record that carries every field, lifting id / watermark / created values out of the
raw dict by field name. The full `values` dict is preserved, so unmapped fields still travel
downstream.

```python
record = build_record(
    raw,
    id_field='Id',
    watermark_field='LastModifiedDate',   # converted to epoch millis
    created_field='CreatedDate',
    name='contact',                       # Record.name = the entity/object name
)

# Use the field names already configured on the connection:
record = build_record_from_connection(raw, connection)
```

`Record.name` is normally the entity/object name (`'contact'`, `'deal'`), so pass it via
`name`. If a per-record label lives in the data instead, pass `name_field='Name'`; when both
are given, the `name_field` value (when present) overrides `name`.

`build_record` accepts watermark / created values as a `datetime`, `date`, ISO string, an int,
or an all-digit string (for example a CSV-sourced `'1609459200000'`); numeric values are
assumed to already be epoch millis. A blank or whitespace-only watermark / created value
(common in CSV) is treated as missing and leaves `lastModified` / `createdAt` unset rather
than raising.

`with_all_fields` projects a raw dict onto every field in the schema. All original keys are
kept; with `fill_missing=True`, schema fields absent from the dict are added as `None`:

```python
with_all_fields(raw, schema, fill_missing=True)
all_field_names(schema)               # ['Id', 'Name', ...]
```

### CSV

```python
records = csv_to_records(csv_text, id_field='Id')
csv_text = records_to_csv(records)
csv_to_records(text, delimiter=';')   # custom delimiter
```

All CSV values are strings (CSV carries no types). Fully blank rows are skipped by default
(`skip_blank=False` keeps them); rows with more cells than headers have the extra unnamed
cells dropped rather than raising. A leading UTF-8 BOM is stripped so the first column name
is not corrupted (and `id_field` / `name_field` still match). When `fieldnames` is omitted,
`records_to_csv` uses the first-seen-order union of all record value keys.
