Skip to content

Commit

Permalink
linting
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianswms committed Jan 3, 2025
1 parent 1284e30 commit 294178c
Show file tree
Hide file tree
Showing 8 changed files with 1,494 additions and 68 deletions.
35 changes: 24 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,29 @@ Built with the [Meltano Tap SDK](https://sdk.meltano.com) for Singer Taps.

### Accepted Config Options

<!--
Developer TODO: Provide a list of config options accepted by the tap.
| Setting | Required | Default | Description |
|:--------|:--------:|:-------:|:------------|
| user_id | True | None | Email address of the user to authenticate with. |
| client_id | True | None | Client ID, obtained from Sunwave support staff. |
| client_secret | True | None | Client secret, obtained from Sunwave support staff. |
| clinic_id | True | None | Clinic ID, obtained by inspecting requests in the browser. |
| stream_maps | False | None | Config object for stream maps capability. For more information check out [Stream Maps](https://sdk.meltano.com/en/latest/stream_maps.html). |
| stream_map_config | False | None | User-defined config values to be used within map expressions. |
| faker_config | False | None | Config for the [`Faker`](https://faker.readthedocs.io/en/master/) instance variable `fake` used within map expressions. Only applicable if the plugin specifies `faker` as an addtional dependency (through the `singer-sdk` `faker` extra or directly). |
| faker_config.seed | False | None | Value to seed the Faker generator for deterministic output: https://faker.readthedocs.io/en/master/#seeding-the-generator |
| faker_config.locale | False | None | One or more LCID locale strings to produce localized output for: https://faker.readthedocs.io/en/master/#localization |
| flattening_enabled | False | None | 'True' to enable schema flattening and automatically expand nested properties. |
| flattening_max_depth | False | None | The max depth to flatten schemas. |
| batch_config | False | None | Configuration for BATCH message capabilities. |
| batch_config.encoding | False | None | Specifies the format and compression of the batch files. |
| batch_config.encoding.format | False | None | Format to use for batch files. |
| batch_config.encoding.compression | False | None | Compression format to use for batch files. |
| batch_config.storage | False | None | Defines the storage layer to use when writing batch files |
| batch_config.storage.root | False | None | Root path to use when writing batch files. |
| batch_config.storage.prefix | False | None | Prefix to use when writing batch files. |

A full list of supported settings and capabilities is available by running: `tap-sunwave --about`

This section can be created by copy-pasting the CLI output from:
```
tap-sunwave --about --format=markdown
```
-->

A full list of supported settings and capabilities for this
tap is available by running:
Expand All @@ -33,9 +47,8 @@ environment variable is set either in the terminal context or in the `.env` file

### Source Authentication and Authorization

<!--
Developer TODO: If your tap requires special access on the source system, or any special authentication requirements, provide those here.
-->
Provide your email address in config as `user_id`. Reach out to Sunwave support staff to get your `client_id` and `client_secret` (see instructions at https://emr.sunwavehealth.com/SunwaveEMR/swagger/).


## Usage

Expand Down
1,433 changes: 1,433 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ requests = "~=2.32.3"
[tool.poetry.group.dev.dependencies]
pytest = ">=8"
singer-sdk = { version="~=0.43.1", extras = ["testing"] }
black = "~=24.10.0"
ruff = "~=0.8.5"

[tool.poetry.extras]
s3 = ["fs-s3fs"]
Expand All @@ -46,6 +48,7 @@ target-version = "py39"
ignore = [
"COM812", # missing-trailing-comma
"ISC001", # single-line-implicit-string-concatenation
"D",
]
select = ["ALL"]

Expand Down
52 changes: 33 additions & 19 deletions tap_sunwave/auth.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,63 @@
"""Sunwave Authentication."""

from __future__ import annotations

import base64
from datetime import datetime, timezone
import hashlib
import hmac
import uuid
import typing as t
import uuid
from datetime import datetime, timezone

import requests
from singer_sdk.authenticators import APIAuthenticatorBase, SingletonMeta

if t.TYPE_CHECKING:
import requests

from tap_sunwave.client import SunwaveStream


class SunwaveAuthenticator(APIAuthenticatorBase, metaclass=SingletonMeta):
"""Authenticator class for Sunwave."""


def authenticate_request(
self,
request: requests.PreparedRequest,
) -> requests.PreparedRequest:

request_body = request.body if request.body else ""

date_calc = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S %z")
dateTimeBase64 = base64.b64encode(date_calc.encode('utf-8')).decode('utf-8')
datetime_base_64 = base64.b64encode(date_calc.encode("utf-8")).decode("utf-8")
unique_transaction_id = str(uuid.uuid4())

md5_payload = hashlib.md5(request_body.encode('utf-8')).hexdigest() # For GET requests this isn't needed, but an empty string md5'd works
base64md5Payload_bytes = base64.b64encode(md5_payload.encode('utf-8'))
base64md5Payload = base64md5Payload_bytes.decode('utf-8').replace('/', '_').replace('+', '-')

seed_string = f"{self.config['user_id']}:{self.config['client_id']}:{dateTimeBase64}:{self.config['clinic_id']}:{unique_transaction_id}:{base64md5Payload}"
seed_bytes = seed_string.encode('utf-8')

hmac_digest = hmac.new(self.config['client_secret'].encode('utf-8'), seed_bytes, hashlib.sha512).digest()
hmacBase64_bytes = base64.b64encode(hmac_digest)
hmacBase64 = hmacBase64_bytes.decode('utf-8').replace('/', '_').replace('+', '-')

request.headers.update({"Authorization": f"Digest {seed_string}:{hmacBase64}"})
# For GET requests this isn't needed, but an empty string md5'd works
md5_payload = hashlib.md5( # noqa: S324
request_body.encode("utf-8")
).hexdigest()
base64_md5_payload_bytes = base64.b64encode(md5_payload.encode("utf-8"))
base64_md5_payload = (
base64_md5_payload_bytes.decode("utf-8").replace("/", "_").replace("+", "-")
)

seed_string = (
f"{self.config['user_id']}:{self.config['client_id']}:{datetime_base_64}:"
f"{self.config['clinic_id']}:{unique_transaction_id}:{base64_md5_payload}"
)
seed_bytes = seed_string.encode("utf-8")

hmac_digest = hmac.new(
self.config["client_secret"].encode("utf-8"), seed_bytes, hashlib.sha512
).digest()
hmac_base64_bytes = base64.b64encode(hmac_digest)
hmac_base64 = (
hmac_base64_bytes.decode("utf-8").replace("/", "_").replace("+", "-")
)

request.headers.update({"Authorization": f"Digest {seed_string}:{hmac_base64}"})

return request

@classmethod
def create_for_stream(cls, stream: SunwaveStream) -> SunwaveAuthenticator: # noqa: ANN001
def create_for_stream(cls, stream: SunwaveStream) -> SunwaveAuthenticator:
return cls(stream=stream)
31 changes: 1 addition & 30 deletions tap_sunwave/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@

from __future__ import annotations

import decimal
import typing as t
from functools import cached_property
from importlib import resources

from singer_sdk.helpers.jsonpath import extract_jsonpath
from singer_sdk.pagination import BaseAPIPaginator # noqa: TC002
from singer_sdk.streams import RESTStream

from tap_sunwave.auth import SunwaveAuthenticator

if t.TYPE_CHECKING:
import requests
from singer_sdk.helpers.types import Auth, Context
from singer_sdk.helpers.types import Auth


SCHEMAS_DIR = resources.files(__package__) / "schemas"
Expand All @@ -24,33 +20,8 @@
class SunwaveStream(RESTStream):
"""Sunwave stream class."""

records_jsonpath = "$[*]"
next_page_token_jsonpath = "$.next_page" # noqa: S105
url_base = "https://emr.sunwavehealth.com/SunwaveEMR"

@cached_property
def authenticator(self) -> Auth:
return SunwaveAuthenticator.create_for_stream(self)

@property
def http_headers(self) -> dict:
return {}

def get_new_paginator(self) -> BaseAPIPaginator:
return super().get_new_paginator()

def get_url_params(
self,
context: Context | None, # noqa: ARG002
next_page_token: t.Any | None, # noqa: ANN401
) -> dict[str, t.Any]:

params: dict = {}
return params

def prepare_request_payload(
self,
context: Context | None, # noqa: ARG002
next_page_token: t.Any | None, # noqa: ARG002, ANN401
) -> dict | None:
return None
2 changes: 0 additions & 2 deletions tap_sunwave/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import typing as t
from importlib import resources

from singer_sdk import typing as th # JSON Schema typing helpers

from tap_sunwave.client import SunwaveStream

SCHEMAS_DIR = resources.files(__package__) / "schemas"
Expand Down
1 change: 0 additions & 1 deletion tap_sunwave/tap.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from singer_sdk import Tap
from singer_sdk import typing as th # JSON schema typing helpers

# TODO: Import your custom stream types here:
from tap_sunwave import streams


Expand Down
5 changes: 0 additions & 5 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
"""Tests standard tap features using the built-in SDK tests library."""

import datetime

from singer_sdk.testing import get_tap_test_class

from tap_sunwave.tap import TapSunwave

def test_tap_sunwave() -> None:
pass

0 comments on commit 294178c

Please sign in to comment.