From ec1938e60216e183e4197ea83152a09d3b8d007a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 12:35:49 +0100
Subject: [PATCH 01/64] added http client
---
ntropy_sdk/http.py | 161 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 161 insertions(+)
create mode 100644 ntropy_sdk/http.py
diff --git a/ntropy_sdk/http.py b/ntropy_sdk/http.py
new file mode 100644
index 0000000..205f2b4
--- /dev/null
+++ b/ntropy_sdk/http.py
@@ -0,0 +1,161 @@
+import logging
+import time
+import uuid
+from json import JSONDecodeError
+from typing import Optional
+
+import requests
+from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
+
+from ntropy_sdk import VERSION
+from ntropy_sdk.v2.errors import error_from_http_status_code, NtropyError
+
+
+class HttpClient:
+ def __init__(self, session: requests.Session | None = None):
+ self._session = session
+
+ def _get_session(self) -> requests.Session:
+ if self._session is None:
+ self._session = requests.Session()
+ self._session.mount("https://", TCPKeepAliveAdapter())
+ return self._session
+
+ @property
+ def session(self) -> requests.Session:
+ return self._get_session()
+
+ @session.setter
+ def session(self, session: requests.Session):
+ self._session = session
+
+ def retry_ratelimited_request(
+ self,
+ method: str,
+ url: str,
+ payload: Optional[object] = None,
+ payload_json_str: Optional[str] = None,
+ logger: logging.Logger | None = None,
+ log_level=logging.DEBUG,
+ request_id: Optional[str] = None,
+ api_key: Optional[str] = None,
+ session: Optional[requests.Session] = None,
+ retries: int = 1,
+ timeout: int = 10 * 60,
+ retry_on_unhandled_exception: bool = False,
+ extra_headers: Optional[dict] = None,
+ **request_kwargs,
+ ):
+ """Executes a request to an endpoint in the Ntropy API (given the `base_url` parameter).
+ Catches expected errors and wraps them in NtropyError.
+ Retries the request for Rate-Limiting errors or Unexpected Errors (50x)
+
+
+ Raises
+ ------
+ NtropyError
+ If the request failed after the maximum number of retries.
+ """
+
+ if payload_json_str is not None and payload is not None:
+ raise ValueError(
+ "payload_json_str and payload cannot be used simultaneously"
+ )
+
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ cur_session = session
+ if cur_session is None:
+ cur_session = self._get_session()
+
+ headers = {
+ "User-Agent": f"ntropy-sdk/{VERSION}",
+ "X-Request-ID": request_id,
+ }
+ if api_key is not None:
+ headers["X-API-Key"] = api_key
+ if payload_json_str is None:
+ request_kwargs["json"] = payload
+ else:
+ headers["Content-Type"] = "application/json"
+ request_kwargs["data"] = payload_json_str
+ if extra_headers:
+ headers.update(extra_headers)
+
+ backoff = 1
+ for _ in range(retries):
+ try:
+ resp = cur_session.request(
+ method,
+ url,
+ headers=headers,
+ timeout=timeout,
+ **request_kwargs,
+ )
+ except requests.ConnectionError:
+ # Rebuild session on connection error and retry
+ if session is None:
+ self._session = None
+ cur_session = self._get_session()
+ continue
+ else:
+ raise
+
+ if resp.status_code == 429:
+ try:
+ retry_after = int(resp.headers.get("retry-after", "1"))
+ except ValueError:
+ retry_after = 1
+ if retry_after <= 0:
+ retry_after = 1
+
+ if logger:
+ logger.log(
+ log_level,
+ "Retrying in %s seconds due to ratelimit",
+ retry_after,
+ )
+ time.sleep(retry_after)
+
+ continue
+ elif resp.status_code == 503:
+ time.sleep(backoff)
+ backoff = min(backoff * 2, 8)
+
+ if logger:
+ logger.log(
+ log_level,
+ "Retrying in %s seconds due to unavailability in the server side",
+ backoff,
+ )
+ continue
+
+ elif (
+ resp.status_code >= 500 and resp.status_code <= 511
+ ) and retry_on_unhandled_exception:
+ time.sleep(backoff)
+ backoff = min(backoff * 2, 8)
+
+ if logger:
+ logger.log(
+ log_level,
+ "Retrying in %s seconds due to unhandled exception in the server side",
+ backoff,
+ )
+
+ continue
+
+ try:
+ resp.raise_for_status()
+ except requests.HTTPError as e:
+ status_code = e.response.status_code
+
+ try:
+ content = e.response.json()
+ except JSONDecodeError:
+ content = {}
+
+ err = error_from_http_status_code(request_id, status_code, content)
+ raise err
+ return resp
+ raise NtropyError(f"Failed to {method} {url} after {retries} attempts")
From bcfeb11fe6b1aa3c5ffb4f1422512a6126bbd5fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 12:36:06 +0100
Subject: [PATCH 02/64] moved old to v2
---
ntropy_sdk/{v3 => }/paging.py | 2 +-
ntropy_sdk/v2/__init__.py | 31 +++++
ntropy_sdk/v2/bank_statements.py | 50 ++++++++
ntropy_sdk/{ => v2}/errors.py | 0
ntropy_sdk/{ => v2}/income_check.py | 0
ntropy_sdk/{ => v2}/ntropy_sdk.py | 132 ++++------------------
ntropy_sdk/{ => v2}/recurring_payments.py | 0
7 files changed, 102 insertions(+), 113 deletions(-)
rename ntropy_sdk/{v3 => }/paging.py (98%)
create mode 100644 ntropy_sdk/v2/__init__.py
create mode 100644 ntropy_sdk/v2/bank_statements.py
rename ntropy_sdk/{ => v2}/errors.py (100%)
rename ntropy_sdk/{ => v2}/income_check.py (100%)
rename ntropy_sdk/{ => v2}/ntropy_sdk.py (95%)
rename ntropy_sdk/{ => v2}/recurring_payments.py (100%)
diff --git a/ntropy_sdk/v3/paging.py b/ntropy_sdk/paging.py
similarity index 98%
rename from ntropy_sdk/v3/paging.py
rename to ntropy_sdk/paging.py
index be80a60..9904890 100644
--- a/ntropy_sdk/v3/paging.py
+++ b/ntropy_sdk/paging.py
@@ -15,7 +15,7 @@
from ntropy_sdk.utils import PYDANTIC_V2
if TYPE_CHECKING:
- from . import ExtraKwargs
+ from ntropy_sdk.v3 import ExtraKwargs
from pydantic import BaseModel as GenericModel
from typing_extensions import Unpack, Self
diff --git a/ntropy_sdk/v2/__init__.py b/ntropy_sdk/v2/__init__.py
new file mode 100644
index 0000000..6565797
--- /dev/null
+++ b/ntropy_sdk/v2/__init__.py
@@ -0,0 +1,31 @@
+from ntropy_sdk.v2.ntropy_sdk import (
+ AccountHolder,
+ AccountHolderType,
+ Transaction,
+ SDK,
+ Batch,
+ EnrichedTransaction,
+ EnrichedTransactionList,
+ BankStatement,
+ BankStatementRequest,
+ Report,
+)
+from ntropy_sdk.v2.errors import (
+ NtropyError,
+ NtropyBatchError,
+)
+
+__all__ = (
+ "AccountHolder",
+ "AccountHolderType",
+ "Transaction",
+ "SDK",
+ "Batch",
+ "NtropyError",
+ "NtropyBatchError",
+ "EnrichedTransaction",
+ "EnrichedTransactionList",
+ "BankStatement",
+ "BankStatementRequest",
+ "Report",
+)
diff --git a/ntropy_sdk/v2/bank_statements.py b/ntropy_sdk/v2/bank_statements.py
new file mode 100644
index 0000000..4d843d9
--- /dev/null
+++ b/ntropy_sdk/v2/bank_statements.py
@@ -0,0 +1,50 @@
+from typing import List, Optional
+from pydantic import BaseModel
+from datetime import date
+
+
+from ntropy_sdk.utils import AccountHolderType
+
+
+class Address(BaseModel):
+ street: Optional[str]
+ postcode: Optional[str]
+ city: Optional[str]
+ state: Optional[str]
+ country: Optional[str]
+
+ class Config:
+ extra = "allow"
+
+
+class AccountHolder(BaseModel):
+ type: Optional[AccountHolderType]
+ name: Optional[str]
+ address: Optional[Address]
+
+ class Config:
+ use_enum_values = True
+ extra = "allow"
+
+
+class Account(BaseModel):
+ type: Optional[str]
+ number: Optional[str]
+ opening_balance: Optional[float]
+ closing_balance: Optional[float]
+ iso_currency_code: Optional[str]
+
+ class Config:
+ extra = "allow"
+
+
+class StatementInfo(BaseModel):
+ institution: Optional[str]
+ start_date: Optional[date]
+ end_date: Optional[date]
+ account_holder: Optional[AccountHolder]
+ accounts: Optional[List[Account]]
+ request_id: str
+
+ class Config:
+ extra = "allow"
diff --git a/ntropy_sdk/errors.py b/ntropy_sdk/v2/errors.py
similarity index 100%
rename from ntropy_sdk/errors.py
rename to ntropy_sdk/v2/errors.py
diff --git a/ntropy_sdk/income_check.py b/ntropy_sdk/v2/income_check.py
similarity index 100%
rename from ntropy_sdk/income_check.py
rename to ntropy_sdk/v2/income_check.py
diff --git a/ntropy_sdk/ntropy_sdk.py b/ntropy_sdk/v2/ntropy_sdk.py
similarity index 95%
rename from ntropy_sdk/ntropy_sdk.py
rename to ntropy_sdk/v2/ntropy_sdk.py
index 946d361..8d31d41 100644
--- a/ntropy_sdk/ntropy_sdk.py
+++ b/ntropy_sdk/v2/ntropy_sdk.py
@@ -38,7 +38,9 @@
from ntropy_sdk import __version__
from ntropy_sdk.bank_statements import StatementInfo
from ntropy_sdk.income_check import IncomeReport, IncomeGroup
-from ntropy_sdk.recurring_payments import (
+
+from ntropy_sdk.http import HttpClient
+from ntropy_sdk.v2.recurring_payments import (
RecurringPaymentsGroups,
RecurringPaymentsGroup,
)
@@ -1262,7 +1264,7 @@ def __init__(
self.base_url = ALL_REGIONS[region]
self.token = token
- self._session: Optional[requests.Session] = None
+ self.http_client = HttpClient()
self.logger = logging.getLogger("Ntropy-SDK")
self.v3 = V3(self)
@@ -1283,20 +1285,6 @@ def _validate_unique_ids(tx_ids: List[str]):
UserWarning,
)
- def _get_session(self) -> requests.Session:
- if self._session is None:
- self._session = requests.Session()
- self._session.mount("https://", TCPKeepAliveAdapter())
- return self._session
-
- @property
- def session(self) -> requests.Session:
- return self._get_session()
-
- @session.setter
- def session(self, session: requests.Session):
- self._session = session
-
def retry_ratelimited_request(
self,
method: str,
@@ -1329,102 +1317,22 @@ def retry_ratelimited_request(
NtropyError
If the request failed after the maximum number of retries.
"""
-
- if payload_json_str is not None and payload is not None:
- raise ValueError("payload_json_str and payload cannot be used simultaneously")
-
- if request_id is None:
- request_id = uuid.uuid4().hex
- if api_key is None:
- api_key = self.token
- cur_session = session
- if cur_session is None:
- cur_session = self._get_session()
-
- headers = {
- "X-API-Key": api_key,
- "User-Agent": f"ntropy-sdk/{__version__}",
- "X-Request-ID": request_id,
- }
- if payload_json_str is None:
- request_kwargs["json"] = payload
- else:
- headers["Content-Type"] = "application/json"
- request_kwargs["data"] = payload_json_str
- headers.update(self._extra_headers)
-
- backoff = 1
- for _ in range(self._retries):
- try:
- resp = cur_session.request(
- method,
- self.base_url + url,
- headers=headers,
- timeout=self._timeout,
- **request_kwargs,
- )
- except requests.ConnectionError:
- # Rebuild session on connection error and retry
- if session is None:
- self._session = None
- cur_session = self._get_session()
- continue
- else:
- raise
-
- if resp.status_code == 429:
- try:
- retry_after = int(resp.headers.get("retry-after", "1"))
- except ValueError:
- retry_after = 1
- if retry_after <= 0:
- retry_after = 1
-
- self.logger.log(
- log_level, "Retrying in %s seconds due to ratelimit", retry_after
- )
- time.sleep(retry_after)
-
- continue
- elif resp.status_code == 503:
- time.sleep(backoff)
- backoff = min(backoff * 2, 8)
-
- self.logger.log(
- log_level,
- "Retrying in %s seconds due to unavailability in the server side",
- backoff,
- )
- continue
-
- elif (
- resp.status_code >= 500 and resp.status_code <= 511
- ) and self._retry_on_unhandled_exception:
- time.sleep(backoff)
- backoff = min(backoff * 2, 8)
-
- self.logger.log(
- log_level,
- "Retrying in %s seconds due to unhandled exception in the server side",
- backoff,
- )
-
- continue
-
- try:
- resp.raise_for_status()
- except requests.HTTPError as e:
- status_code = e.response.status_code
-
- try:
- content = e.response.json()
- except JSONDecodeError:
- content = {}
-
- err = error_from_http_status_code(request_id, status_code, content)
- raise err
- return resp
- raise NtropyError(f"Failed to {method} {url} after {self._retries} attempts")
+ return self.http_client.retry_ratelimited_request(
+ method=method,
+ url=self.base_url + url,
+ payload=payload,
+ payload_json_str=payload_json_str,
+ log_level=log_level,
+ request_id=request_id,
+ api_key=api_key or self.token,
+ session=session,
+ request_kwargs=request_kwargs,
+ logger=self.logger,
+ retries=self._retries,
+ timeout=self._timeout,
+ retry_on_unhandled_exception=self._retry_on_unhandled_exception,
+ extra_headers=self._extra_headers,
+ )
def create_account_holder(self, account_holder: AccountHolder):
"""Creates an AccountHolder for the current user.
diff --git a/ntropy_sdk/recurring_payments.py b/ntropy_sdk/v2/recurring_payments.py
similarity index 100%
rename from ntropy_sdk/recurring_payments.py
rename to ntropy_sdk/v2/recurring_payments.py
From 5cea972fbfbf9f2da08207753373055187dd68c5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 12:36:28 +0100
Subject: [PATCH 03/64] moved v3 to top-level
---
ntropy_sdk/__init__.py | 32 +---
ntropy_sdk/{v3 => }/account_holders.py | 8 +-
ntropy_sdk/bank_statements.py | 227 +++++++++++++++++++++----
ntropy_sdk/{v3 => }/batches.py | 8 +-
ntropy_sdk/{v3 => }/transactions.py | 8 +-
ntropy_sdk/utils.py | 2 +
ntropy_sdk/v3/__init__.py | 10 +-
ntropy_sdk/v3/bank_statements.py | 215 -----------------------
tests/test_datasources.py | 2 +-
tests/test_sdk.py | 3 +-
10 files changed, 221 insertions(+), 294 deletions(-)
rename ntropy_sdk/{v3 => }/account_holders.py (95%)
rename ntropy_sdk/{v3 => }/batches.py (96%)
rename ntropy_sdk/{v3 => }/transactions.py (98%)
delete mode 100644 ntropy_sdk/v3/bank_statements.py
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 88739af..b558e4d 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -1,31 +1,3 @@
-__version__ = "4.26.0"
+from .version import VERSION
-from ntropy_sdk.ntropy_sdk import (
- AccountHolder,
- AccountHolderType,
- Transaction,
- SDK,
- Batch,
- NtropyError,
- NtropyBatchError,
- EnrichedTransaction,
- EnrichedTransactionList,
- BankStatement,
- BankStatementRequest,
- Report,
-)
-
-__all__ = (
- "AccountHolder",
- "AccountHolderType",
- "Transaction",
- "SDK",
- "Batch",
- "NtropyError",
- "NtropyBatchError",
- "EnrichedTransaction",
- "EnrichedTransactionList",
- "BankStatement",
- "BankStatementRequest",
- "Report",
-)
+__version__ = VERSION
diff --git a/ntropy_sdk/v3/account_holders.py b/ntropy_sdk/account_holders.py
similarity index 95%
rename from ntropy_sdk/v3/account_holders.py
rename to ntropy_sdk/account_holders.py
index eafceca..e6c59a3 100644
--- a/ntropy_sdk/v3/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -6,11 +6,11 @@
from pydantic import BaseModel, Field
from ntropy_sdk.utils import pydantic_json
-from ntropy_sdk.v3.paging import PagedResponse
+from ntropy_sdk.paging import PagedResponse
if TYPE_CHECKING:
- from ntropy_sdk.ntropy_sdk import SDK
- from . import ExtraKwargs
+ from ntropy_sdk.v2.ntropy_sdk import SDK
+ from ntropy_sdk.v3 import ExtraKwargs
from typing_extensions import Unpack
@@ -107,3 +107,5 @@ def create(
**extra_kwargs,
)
return AccountHolder(**resp.json(), request_id=request_id)
+
+ # TODO: Recurring groups
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 4d843d9..194a1a4 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -1,50 +1,217 @@
-from typing import List, Optional
-from pydantic import BaseModel
-from datetime import date
+from datetime import date, datetime
+from enum import Enum
+from io import IOBase
+import time
+from typing import List, Optional, TYPE_CHECKING, Union
+import uuid
+from pydantic import BaseModel, Field, NonNegativeFloat
-from ntropy_sdk.utils import AccountHolderType
+from ntropy_sdk.bank_statements import StatementInfo
+from ntropy_sdk.errors import NtropyDatasourceError
+from ntropy_sdk.utils import EntryType
+from ntropy_sdk.paging import PagedResponse
+if TYPE_CHECKING:
+ from ntropy_sdk.v2.ntropy_sdk import SDK
+ from ntropy_sdk.v3 import ExtraKwargs
+ from typing_extensions import Unpack
-class Address(BaseModel):
- street: Optional[str]
- postcode: Optional[str]
- city: Optional[str]
- state: Optional[str]
- country: Optional[str]
- class Config:
- extra = "allow"
+class BankStatementJobStatus(str, Enum):
+ PROCESSING = "processing"
+ COMPLETED = "completed"
+ ERROR = "error"
+
+class BankStatementFile(BaseModel):
+ no_pages: int
+ size: Optional[int]
-class AccountHolder(BaseModel):
- type: Optional[AccountHolderType]
+
+class BankStatementJob(BaseModel):
+ id: str
name: Optional[str]
- address: Optional[Address]
+ status: BankStatementJobStatus
+ created_at: datetime
+ file: BankStatementFile
+ request_id: Optional[str] = None
+
+ def is_completed(self):
+ ...
+
+ def is_failed(self):
+ ...
+
+ def wait_for_results(
+ self,
+ sdk: "SDK",
+ *,
+ timeout: int = 4 * 60 * 60,
+ poll_interval: int = 10,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> "BankStatementResults":
+ """Continuously polls the status of this job, blocking until the job either succeeds
+ or fails. If the job is successful, returns the results. Otherwise, raises an
+ `NtropyDatasourceError` exception."""
+
+ finish_statuses = [
+ BankStatementJobStatus.COMPLETED,
+ BankStatementJobStatus.ERROR,
+ ]
+ start_time = time.monotonic()
+ while time.monotonic() - start_time < timeout:
+ self.status = sdk.v3.bank_statements.get(id=self.id).status
+ if self.status in finish_statuses:
+ break
+ time.sleep(poll_interval)
+
+ if self.status is BankStatementJobStatus.COMPLETED:
+ return sdk.v3.bank_statements.results(id=self.id, **extra_kwargs)
+ else:
+ raise NtropyDatasourceError()
class Config:
- use_enum_values = True
extra = "allow"
-class Account(BaseModel):
- type: Optional[str]
+class BankStatementTransaction(BaseModel):
+ date: date
+ entry_type: EntryType
+ amount: NonNegativeFloat
+ running_balance: Optional[float]
+ iso_currency_code: str = Field(
+ description="The currency of the transaction in ISO 4217 format"
+ )
+ description: str
+ transaction_id: str = Field(
+ description="A generated unique identifier for the transaction", min_length=1
+ )
+
+
+class BankStatementAccount(BaseModel):
number: Optional[str]
opening_balance: Optional[float]
closing_balance: Optional[float]
- iso_currency_code: Optional[str]
+ start_date: Optional[date]
+ end_date: Optional[date]
+ is_balance_reconciled: Optional[bool]
+ total_incoming: Optional[float]
+ total_outgoing: Optional[float]
+ transactions: List[BankStatementTransaction]
- class Config:
- extra = "allow"
+class BankStatementResults(BankStatementJob):
+ accounts: List[BankStatementAccount]
-class StatementInfo(BaseModel):
- institution: Optional[str]
- start_date: Optional[date]
- end_date: Optional[date]
- account_holder: Optional[AccountHolder]
- accounts: Optional[List[Account]]
- request_id: str
- class Config:
- extra = "allow"
+class BankStatementsResource:
+ def __init__(self, sdk: "SDK"):
+ self._sdk = sdk
+
+ def list(
+ self,
+ *,
+ created_before: Optional[datetime] = None,
+ created_after: Optional[datetime] = None,
+ cursor: Optional[str] = None,
+ limit: Optional[int] = None,
+ status: Optional[BankStatementJobStatus] = None,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> PagedResponse[BankStatementJob]:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ "GET",
+ "/v3/bank_statements",
+ params={
+ "created_before": created_before,
+ "created_after": created_after,
+ "cursor": cursor,
+ "limit": limit,
+ "status": status.value if status else None,
+ },
+ payload=None,
+ **extra_kwargs,
+ )
+ page = PagedResponse[BankStatementJob](
+ **resp.json(),
+ request_id=request_id,
+ _resource=self,
+ _extra_kwargs=extra_kwargs,
+ )
+ for b in page.data:
+ b.request_id = request_id
+ return page
+
+ def create(
+ self,
+ *,
+ file: Union[IOBase, bytes],
+ filename: Optional[str] = None,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> BankStatementJob:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ "POST",
+ "/v3/bank_statements",
+ payload=None,
+ files={
+ "file": file if filename is None else (filename, file),
+ },
+ **extra_kwargs,
+ )
+ return BankStatementJob(**resp.json(), request_id=request_id)
+
+ def get(
+ self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
+ ) -> BankStatementJob:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ "GET",
+ f"/v3/bank_statements/{id}",
+ payload=None,
+ **extra_kwargs,
+ )
+ return BankStatementJob(**resp.json(), request_id=request_id)
+
+ def results(
+ self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
+ ) -> BankStatementResults:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ "GET",
+ f"/v3/bank_statements/{id}/results",
+ payload=None,
+ **extra_kwargs,
+ )
+ return BankStatementResults(**resp.json(), request_id=request_id)
+
+ def overview(
+ self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
+ ) -> StatementInfo:
+ """Waits for and returns preliminary statement information from the
+ first page of the PDF. This may not always be consistent with the
+ final results."""
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ "GET",
+ f"/v3/bank_statements/{id}/overview",
+ payload=None,
+ **extra_kwargs,
+ )
+ return StatementInfo(**resp.json(), request_id=request_id)
diff --git a/ntropy_sdk/v3/batches.py b/ntropy_sdk/batches.py
similarity index 96%
rename from ntropy_sdk/v3/batches.py
rename to ntropy_sdk/batches.py
index 618395e..81d3135 100644
--- a/ntropy_sdk/v3/batches.py
+++ b/ntropy_sdk/batches.py
@@ -8,12 +8,12 @@
from ntropy_sdk.errors import NtropyBatchError
from ntropy_sdk.utils import pydantic_json
-from ntropy_sdk.v3.paging import PagedResponse
-from ntropy_sdk.v3.transactions import EnrichedTransaction, EnrichmentInput
+from ntropy_sdk.paging import PagedResponse
+from ntropy_sdk.transactions import EnrichedTransaction, EnrichmentInput
if TYPE_CHECKING:
- from ntropy_sdk.ntropy_sdk import SDK
- from . import ExtraKwargs
+ from ntropy_sdk.v2.ntropy_sdk import SDK
+ from ntropy_sdk.v3 import ExtraKwargs
from typing_extensions import Unpack
diff --git a/ntropy_sdk/v3/transactions.py b/ntropy_sdk/transactions.py
similarity index 98%
rename from ntropy_sdk/v3/transactions.py
rename to ntropy_sdk/transactions.py
index 109e342..ba0d135 100644
--- a/ntropy_sdk/v3/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -6,7 +6,7 @@
from pydantic import BaseModel, Field, NonNegativeFloat
from ntropy_sdk.utils import EntryType, PYDANTIC_V2, pydantic_json
-from ntropy_sdk.v3.paging import PagedResponse
+from ntropy_sdk.paging import PagedResponse
PYDANTIC_PATTERN = "pattern" if PYDANTIC_V2 else "regex"
MAX_SYNC_BATCH = 1000
@@ -14,7 +14,7 @@
if TYPE_CHECKING:
- from ntropy_sdk.ntropy_sdk import SDK
+ from ntropy_sdk.v2.ntropy_sdk import SDK
from ntropy_sdk.v3 import ExtraKwargs
from typing_extensions import Unpack
@@ -62,7 +62,7 @@ class _TransactionBase(BaseModel):
)
-class TransactionInput(_TransactionBase):
+class InputTransaction(_TransactionBase):
account_holder_id: Optional[str] = Field(
None,
description="The id of the account holder. Unsetting it will disable categorization.",
@@ -280,7 +280,7 @@ class EnrichedTransaction(_EnrichedTransactionBase):
class EnrichmentInput(BaseModel):
- transactions: List[TransactionInput]
+ transactions: List[InputTransaction]
class EnrichmentResult(BaseModel):
diff --git a/ntropy_sdk/utils.py b/ntropy_sdk/utils.py
index 9bb50bf..6b49704 100644
--- a/ntropy_sdk/utils.py
+++ b/ntropy_sdk/utils.py
@@ -7,6 +7,7 @@
PYDANTIC_V2 = pydantic.VERSION.startswith("2.")
if PYDANTIC_V2:
+
class PydanticList(pydantic.RootModel[Any]): # type: ignore
pass
@@ -15,6 +16,7 @@ def pydantic_list_json(x: List[Any]) -> str: # type: ignore
def pydantic_json(m: pydantic.BaseModel) -> str:
return m.model_dump_json()
+
else:
import pydantic.generics
diff --git a/ntropy_sdk/v3/__init__.py b/ntropy_sdk/v3/__init__.py
index e7c16a0..cc7812a 100644
--- a/ntropy_sdk/v3/__init__.py
+++ b/ntropy_sdk/v3/__init__.py
@@ -3,7 +3,7 @@
if TYPE_CHECKING:
- from ntropy_sdk.ntropy_sdk import SDK
+ from ntropy_sdk.v2.ntropy_sdk import SDK
from typing_extensions import TypedDict
class ExtraKwargs(TypedDict, total=False):
@@ -12,10 +12,10 @@ class ExtraKwargs(TypedDict, total=False):
session: Optional[requests.Session]
-from .transactions import TransactionsResource
-from .batches import BatchesResource
-from .bank_statements import BankStatementsResource
-from .account_holders import AccountHoldersResource
+from ntropy_sdk.transactions import TransactionsResource
+from ntropy_sdk.batches import BatchesResource
+from ntropy_sdk.bank_statements import BankStatementsResource
+from ntropy_sdk.account_holders import AccountHoldersResource
class V3:
diff --git a/ntropy_sdk/v3/bank_statements.py b/ntropy_sdk/v3/bank_statements.py
deleted file mode 100644
index e6af422..0000000
--- a/ntropy_sdk/v3/bank_statements.py
+++ /dev/null
@@ -1,215 +0,0 @@
-from datetime import date, datetime
-from enum import Enum
-from io import IOBase
-import time
-from typing import List, Optional, TYPE_CHECKING, Union
-import uuid
-
-from pydantic import BaseModel, Field, NonNegativeFloat
-
-from ntropy_sdk.bank_statements import StatementInfo
-from ntropy_sdk.errors import NtropyDatasourceError
-from ntropy_sdk.utils import EntryType
-from ntropy_sdk.v3.paging import PagedResponse
-
-if TYPE_CHECKING:
- from ntropy_sdk.ntropy_sdk import SDK
- from . import ExtraKwargs
- from typing_extensions import Unpack
-
-
-class BankStatementJobStatus(str, Enum):
- PROCESSING = "processing"
- COMPLETED = "completed"
- ERROR = "error"
-
-
-class BankStatementFile(BaseModel):
- no_pages: int
- size: Optional[int]
-
-
-class BankStatementJob(BaseModel):
- id: str
- name: Optional[str]
- status: BankStatementJobStatus
- created_at: datetime
- file: BankStatementFile
- request_id: Optional[str] = None
-
- def wait(
- self,
- sdk: "SDK",
- *,
- timeout: int = 4 * 60 * 60,
- poll_interval: int = 10,
- **extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> "BankStatementResults":
- """Continuously polls the status of this job, blocking until the job either succeeds
- or fails. If the job is successful, returns the results. Otherwise, raises an
- `NtropyDatasourceError` exception."""
-
- finish_statuses = [
- BankStatementJobStatus.COMPLETED,
- BankStatementJobStatus.ERROR,
- ]
- start_time = time.monotonic()
- while time.monotonic() - start_time < timeout:
- self.status = sdk.v3.bank_statements.get(id=self.id).status
- if self.status in finish_statuses:
- break
- time.sleep(poll_interval)
-
- if self.status is BankStatementJobStatus.COMPLETED:
- return sdk.v3.bank_statements.results(id=self.id, **extra_kwargs)
- else:
- raise NtropyDatasourceError()
-
- def overview(self, sdk: "SDK") -> StatementInfo:
- """Convenience function for `sdk.v3.bank_statements.overview`."""
- return sdk.v3.bank_statements.overview(id=self.id)
-
- class Config:
- extra = "allow"
-
-
-class BankStatementTransaction(BaseModel):
- date: date
- entry_type: EntryType
- amount: NonNegativeFloat
- running_balance: Optional[float]
- iso_currency_code: str = Field(
- description="The currency of the transaction in ISO 4217 format"
- )
- description: str
- transaction_id: str = Field(
- description="A generated unique identifier for the transaction", min_length=1
- )
-
-
-class BankStatementAccount(BaseModel):
- number: Optional[str]
- opening_balance: Optional[float]
- closing_balance: Optional[float]
- start_date: Optional[date]
- end_date: Optional[date]
- is_balance_reconciled: Optional[bool]
- total_incoming: Optional[float]
- total_outgoing: Optional[float]
- transactions: List[BankStatementTransaction]
-
-
-class BankStatementResults(BankStatementJob):
- accounts: List[BankStatementAccount]
-
-
-class BankStatementsResource:
- def __init__(self, sdk: "SDK"):
- self._sdk = sdk
-
- def list(
- self,
- *,
- created_before: Optional[datetime] = None,
- created_after: Optional[datetime] = None,
- cursor: Optional[str] = None,
- limit: Optional[int] = None,
- status: Optional[BankStatementJobStatus] = None,
- **extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> PagedResponse[BankStatementJob]:
- request_id = extra_kwargs.get("request_id")
- if request_id is None:
- request_id = uuid.uuid4().hex
- extra_kwargs["request_id"] = request_id
- resp = self._sdk.retry_ratelimited_request(
- "GET",
- "/v3/bank_statements",
- params={
- "created_before": created_before,
- "created_after": created_after,
- "cursor": cursor,
- "limit": limit,
- "status": status.value if status else None,
- },
- payload=None,
- **extra_kwargs,
- )
- page = PagedResponse[BankStatementJob](
- **resp.json(),
- request_id=request_id,
- _resource=self,
- _extra_kwargs=extra_kwargs,
- )
- for b in page.data:
- b.request_id = request_id
- return page
-
- def create(
- self,
- *,
- file: Union[IOBase, bytes],
- filename: Optional[str] = None,
- **extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> BankStatementJob:
- request_id = extra_kwargs.get("request_id")
- if request_id is None:
- request_id = uuid.uuid4().hex
- extra_kwargs["request_id"] = request_id
- resp = self._sdk.retry_ratelimited_request(
- "POST",
- "/v3/bank_statements",
- payload=None,
- files={
- "file": file if filename is None else (filename, file),
- },
- **extra_kwargs,
- )
- return BankStatementJob(**resp.json(), request_id=request_id)
-
- def get(
- self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
- ) -> BankStatementJob:
- request_id = extra_kwargs.get("request_id")
- if request_id is None:
- request_id = uuid.uuid4().hex
- extra_kwargs["request_id"] = request_id
- resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/bank_statements/{id}",
- payload=None,
- **extra_kwargs,
- )
- return BankStatementJob(**resp.json(), request_id=request_id)
-
- def results(
- self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
- ) -> BankStatementResults:
- request_id = extra_kwargs.get("request_id")
- if request_id is None:
- request_id = uuid.uuid4().hex
- extra_kwargs["request_id"] = request_id
- resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/bank_statements/{id}/results",
- payload=None,
- **extra_kwargs,
- )
- return BankStatementResults(**resp.json(), request_id=request_id)
-
- def overview(
- self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
- ) -> StatementInfo:
- """Waits for and returns preliminary statement information from the
- first page of the PDF. This may not always be consistent with the
- final results."""
- request_id = extra_kwargs.get("request_id")
- if request_id is None:
- request_id = uuid.uuid4().hex
- extra_kwargs["request_id"] = request_id
- resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/bank_statements/{id}/overview",
- payload=None,
- **extra_kwargs,
- )
- return StatementInfo(**resp.json(), request_id=request_id)
diff --git a/tests/test_datasources.py b/tests/test_datasources.py
index 3ef23af..91a01fb 100644
--- a/tests/test_datasources.py
+++ b/tests/test_datasources.py
@@ -3,7 +3,7 @@
import pytest
from ntropy_sdk.errors import NtropyDatasourceError
-from ntropy_sdk.ntropy_sdk import BankStatementRequest
+from ntropy_sdk.v2.ntropy_sdk import BankStatementRequest
from ntropy_sdk.utils import AccountHolderType
diff --git a/tests/test_sdk.py b/tests/test_sdk.py
index 90cdbb9..ac6dfb9 100644
--- a/tests/test_sdk.py
+++ b/tests/test_sdk.py
@@ -13,11 +13,10 @@
EnrichedTransaction,
SDK,
Transaction,
- NtropyError,
)
from ntropy_sdk.errors import NtropyValueError, NtropyBatchError
from ntropy_sdk.utils import TransactionType
-from ntropy_sdk.ntropy_sdk import ACCOUNT_HOLDER_TYPES
+from ntropy_sdk.v2.ntropy_sdk import ACCOUNT_HOLDER_TYPES
from tests import API_KEY
From 409a3bcc481029760c337057020fc3a1935184ef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 12:36:42 +0100
Subject: [PATCH 04/64] version
---
ntropy_sdk/version.py | 1 +
1 file changed, 1 insertion(+)
create mode 100644 ntropy_sdk/version.py
diff --git a/ntropy_sdk/version.py b/ntropy_sdk/version.py
new file mode 100644
index 0000000..3486778
--- /dev/null
+++ b/ntropy_sdk/version.py
@@ -0,0 +1 @@
+VERSION = "4.26.0"
From 257a366dff8d894ba333e25e2a5472b9e4a4fc48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 12:52:47 +0100
Subject: [PATCH 05/64] moved tests
---
tests/test_sdk.py | 857 +-----------------
tests/test_v3.py | 10 -
tests/{ => v2}/bank_statement_sample.pdf | Bin
tests/{ => v2}/conftest.py | 0
.../test_bank_statements.py} | 2 +-
tests/{ => v2}/test_income_check.py | 2 +-
tests/{ => v2}/test_recurring_payments.py | 2 +-
tests/v2/test_sdk.py | 851 +++++++++++++++++
8 files changed, 862 insertions(+), 862 deletions(-)
delete mode 100644 tests/test_v3.py
rename tests/{ => v2}/bank_statement_sample.pdf (100%)
rename tests/{ => v2}/conftest.py (100%)
rename tests/{test_datasources.py => v2/test_bank_statements.py} (96%)
rename tests/{ => v2}/test_income_check.py (99%)
rename tests/{ => v2}/test_recurring_payments.py (98%)
create mode 100644 tests/v2/test_sdk.py
diff --git a/tests/test_sdk.py b/tests/test_sdk.py
index ac6dfb9..27947d4 100644
--- a/tests/test_sdk.py
+++ b/tests/test_sdk.py
@@ -1,851 +1,10 @@
-import os
-import uuid
-from decimal import Decimal
-from unittest.mock import patch
+from itertools import islice
+from ntropy_sdk import SDK
-import pandas as pd
-import pytest
-from requests import Response
-from ntropy_sdk import (
- AccountHolder,
- Batch,
- EnrichedTransaction,
- SDK,
- Transaction,
-)
-from ntropy_sdk.errors import NtropyValueError, NtropyBatchError
-from ntropy_sdk.utils import TransactionType
-from ntropy_sdk.v2.ntropy_sdk import ACCOUNT_HOLDER_TYPES
-from tests import API_KEY
-
-
-def test_account_holder_type():
- def create_account_holder(account_holder_type):
- return AccountHolder(
- id=str(uuid.uuid4()),
- type=account_holder_type,
- industry="fintech",
- website="ntropy.com",
- )
-
- for t in ACCOUNT_HOLDER_TYPES:
- account_holder = create_account_holder(t)
- assert account_holder.type == t
- with pytest.raises(ValueError):
- create_account_holder("not_valid")
-
-
-def test_get_account_holder(sdk):
- account_holder = AccountHolder(
- id=str(uuid.uuid4()),
- type="business",
- industry="fintech",
- website="ntropy.com",
- )
-
- sdk.create_account_holder(account_holder)
- account_holder2 = sdk.get_account_holder(account_holder.id)
-
- assert isinstance(account_holder2, AccountHolder)
- assert account_holder2.type == "business"
- assert account_holder2.industry == "fintech"
- assert account_holder2.website == "ntropy.com"
-
-
-def test_account_holder_type_or_id(sdk):
- id_tx = Transaction(
- amount=24.56,
- description="TARGET T- 5800 20th St 11/30/19 17:32",
- entry_type="debit",
- date="2012-12-10",
- account_holder_id="1",
- iso_currency_code="USD",
- mcc=5432,
- )
- enriched = sdk.add_transactions([id_tx])[0]
- assert "missing account holder information" not in enriched.labels
-
- type_tx = Transaction(
- amount=24.56,
- description="TARGET T- 5800 20th St 11/30/19 17:32",
- entry_type="debit",
- date="2012-12-10",
- account_holder_type="business",
- iso_currency_code="USD",
- mcc=5432,
- )
- enriched = sdk.add_transactions([type_tx])[0]
- assert "missing account holder information" not in enriched.labels
-
-
-def test_account_holder_type_or_id_pandas(sdk):
- account_holder = AccountHolder(
- id=str(uuid.uuid4()), type="business", industry="fintech", website="ntropy.com"
- )
- sdk.create_account_holder(account_holder)
-
- df = pd.DataFrame(
- data={
- "amount": [26],
- "description": ["TARGET T- 5800 20th St 11/30/19 17:32"],
- "entry_type": ["debit"],
- "date": ["2012-12-10"],
- "account_holder_id": [account_holder.id],
- "iso_currency_code": ["USD"],
- }
- )
- enriched = sdk.add_transactions(df)
- assert "missing account holder information" not in enriched.labels[0]
-
- df = pd.DataFrame(
- {
- "amount": [27],
- "description": ["TARGET T- 5800 20th St 11/30/19 17:32"],
- "entry_type": ["debit"],
- "date": ["2012-12-10"],
- "account_holder_type": ["business"],
- "iso_currency_code": ["USD"],
- }
- )
- enriched = sdk.add_transactions(df)
- assert "missing account holder information" not in enriched.labels[0]
-
-
-def test_dataframe_inplace(sdk):
- df = pd.DataFrame(
- data={
- "amount": [26],
- "description": ["TARGET T- 5800 20th St 11/30/19 17:32"],
- "entry_type": ["debit"],
- "date": ["2012-12-10"],
- "account_holder_id": [str(uuid.uuid4())],
- "account_holder_type": ["consumer"],
- "iso_currency_code": ["USD"],
- }
- )
- original_columns = df.columns
- # inplace = False should NOT change the original dataframe
- enriched = sdk.add_transactions(df, inplace=False)
- assert list(df.columns) == list(original_columns)
- assert "goods" in enriched.labels[0]
-
- # inplace = True should change the original dataframe
- sdk.add_transactions(df, inplace=True)
- assert "goods" in df.labels[0]
-
-
-def test_account_holder_type_or_id_iterable(sdk):
- id_tx = Transaction(
- amount=24.56,
- description="TARGET T- 5800 20th St 11/30/19 17:32",
- entry_type="debit",
- date="2012-12-10",
- account_holder_id="1",
- iso_currency_code="USD",
- mcc=5432,
- )
- enriched = sdk.add_transactions((t for t in [id_tx]))[0]
- assert "missing account holder information" not in enriched.labels
-
- type_tx = Transaction(
- amount=24.56,
- description="TARGET T- 5800 20th St 11/30/19 17:32",
- entry_type="debit",
- date="2012-12-10",
- account_holder_type="business",
- iso_currency_code="USD",
- mcc=5432,
- )
- enriched = sdk.add_transactions((t for t in [type_tx]))[0]
- assert "missing account holder information" not in enriched.labels
-
-
-def test_bad_date():
- def create_tx(date):
- return Transaction(
- amount=24.56,
- description="TARGET T- 5800 20th St 11/30/19 17:32",
- entry_type="debit",
- date=date,
- account_holder_id="1",
- iso_currency_code="USD",
- )
-
- assert isinstance(create_tx("2021-12-13"), Transaction)
-
- with pytest.raises(ValueError):
- create_tx("bad date")
-
- with pytest.raises(ValueError):
- create_tx("")
-
- with pytest.raises(ValueError):
- create_tx(None)
-
-
-def test_fields():
- tx = Transaction(
- amount=24.56,
- description="TARGET T- 5800 20th St 11/30/19 17:32",
- entry_type="debit",
- date="2012-12-10",
- account_holder_id="1",
- iso_currency_code="USD",
- transaction_id="one-two-three",
- mcc=5432,
- )
-
- assert tx.to_dict() == {
- "amount": 24.56,
- "description": "TARGET T- 5800 20th St 11/30/19 17:32",
- "entry_type": "debit",
- "date": "2012-12-10",
- "iso_currency_code": "USD",
- "transaction_id": "one-two-three",
- "mcc": 5432,
- "account_holder_id": "1",
- }
-
- with pytest.raises(ValueError):
- tx = Transaction(
- amount=float("nan"),
- description="TARGET T- 5800 20th St 11/30/19 17:32",
- entry_type="debit",
- date="2012-12-10",
- account_holder_id="1",
- iso_currency_code="USD",
- transaction_id="one-two-three",
- mcc=5432,
- )
-
-
-def test_enrich_huge_batch(sdk):
- account_holder = AccountHolder(
- id=str(uuid.uuid4()), type="business", industry="fintech", website="ntropy.com"
- )
- sdk.create_account_holder(account_holder)
-
- tx = Transaction(
- amount=24.56,
- description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
- entry_type="debit",
- date="2012-12-10",
- account_holder_id=account_holder.id,
- iso_currency_code="USD",
- )
-
- txs = [tx] * 10
- sdk.MAX_BATCH_SIZE = 4
-
- enriched_txs = sdk.add_transactions(txs)
-
- assert len(enriched_txs) == len(txs)
-
- for i, enriched_tx in enumerate(enriched_txs):
- assert isinstance(enriched_tx, EnrichedTransaction)
- assert enriched_tx.merchant is not None
- assert enriched_tx.transaction_id == txs[i].transaction_id
- assert enriched_tx.parent_tx is txs[i]
-
-
-# TODO: temporarily disabled until persistence timing is adjusted for reports
-# def test_report(sdk):
-# account_holder = AccountHolder(
-# id=str(uuid.uuid4()), type="business", industry="fintech", website="ntropy.com"
-# )
-# sdk.create_account_holder(account_holder)
-#
-# tx = Transaction(
-# amount=24.56,
-# description="TARGET T- 5800 20th St 11/30/19 17:32",
-# entry_type="debit",
-# date="2012-12-10",
-# account_holder_id=account_holder.id,
-# iso_currency_code="USD",
-# )
-# enriched_tx = sdk.add_transactions([tx])[0]
-#
-# enriched_tx.create_report(website="ww2.target.com")
-# enriched_tx.create_report(unplanned_kwarg="bar")
-
-
-def test_hierarchy(sdk):
- for account_holder_type in ["business", "consumer", "unknown"]:
- h = sdk.get_labels(account_holder_type)
- assert isinstance(h, dict)
-
-
-def test_transaction_zero_amount():
- vals = {
- "description": "foo",
- "date": "2021-12-12",
- "entry_type": "debit",
- "account_holder_id": "1",
- "country": "US",
- "iso_currency_code": "USD",
- }
-
- Transaction(amount=0, **vals)
- Transaction(amount=1, **vals)
-
- with pytest.raises(ValueError):
- Transaction(amount=-1, **vals)
-
-
-def test_transaction_entry_type():
- for et in ["incoming", "outgoing", "debit", "credit"]:
- Transaction(
- amount=1.0,
- description="foo",
- date="2012-12-10",
- entry_type=et,
- account_holder_id="bar",
- iso_currency_code="USD",
- country="US",
- )
-
- with pytest.raises(ValueError):
- Transaction(
- amount=1.0,
- description="foo",
- date="2012-12-10",
- entry_type="bar",
- account_holder_id="bar",
- iso_currency_code="bla",
- country="FOO",
- )
-
-
-def test_readme():
- readme_file = open(
- os.path.join(os.path.dirname(__file__), "..", "README.md")
- ).read()
- readme_data = readme_file.split("```python")[1].split("```")[0]
- readme_data = readme_data.replace("YOUR-API-KEY", API_KEY)
- exec(readme_data, globals())
-
-
-def test_add_transactions_async(sdk):
- tx = Transaction(
- amount=24.56,
- description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
- entry_type="debit",
- date="2012-12-10",
- account_holder_type="business",
- iso_currency_code="USD",
- )
-
- batch = sdk.add_transactions_async([tx])
- assert batch.batch_id and len(batch.batch_id) > 0
-
- enriched = batch.wait()
- assert enriched[0].merchant == "Amazon Web Services"
- assert enriched[0].parent_tx is tx
-
-
-def test_add_transactions_async_df(sdk):
- df = pd.DataFrame(
- data={
- "amount": [26],
- "description": ["TARGET T- 5800 20th St 11/30/19 17:32"],
- "entry_type": ["debit"],
- "date": ["2012-12-10"],
- "account_holder_type": ["business"],
- "iso_currency_code": ["USD"],
- }
- )
- batch = sdk.add_transactions_async(df)
- enriched = batch.wait()
- assert enriched[0].merchant == "Target"
-
-
-def test_add_transactions_async_iterable(sdk):
- tx = Transaction(
- amount=24.56,
- description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
- entry_type="debit",
- date="2012-12-10",
- account_holder_type="business",
- iso_currency_code="USD",
- )
-
- batch = sdk.add_transactions_async((t for t in [tx]))
- assert batch.batch_id and len(batch.batch_id) > 0
-
- enriched = batch.wait()
- assert enriched[0].merchant == "Amazon Web Services"
-
-
-def test_batch(sdk):
- tx = Transaction(
- amount=24.56,
- description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
- entry_type="debit",
- date="2012-12-10",
- account_holder_type="business",
- iso_currency_code="USD",
- )
-
- batch = sdk.add_transactions_async([tx] * 10)
- resp, status = batch.poll()
-
- if status != "finished":
- # Might've finished already
- assert status == "started" and resp["total"] == 10
-
- batch.wait()
-
- resp, status = batch.poll()
- assert status == "finished" and resp[0].merchant == "Amazon Web Services"
-
- batch = Batch(sdk=sdk, batch_id=batch.batch_id)
- resp, status = batch.poll()
- assert status == "finished" and resp[0].merchant == "Amazon Web Services"
-
-
-def test_numerical_support():
- tx = Transaction(
- amount=Decimal(24.56),
- description="TARGET T- 5800 20th St 11/30/19 17:32",
- entry_type="debit",
- date="2012-12-10",
- account_holder_id="1",
- iso_currency_code="USD",
- mcc="5432",
- )
-
- assert isinstance(tx.amount, float)
- assert isinstance(tx.mcc, int)
-
-
-def test_none_in_txns_error(sdk):
- test_values = [[None]]
- for test_value in test_values:
- try:
- sdk.add_transactions(test_value)
- assert False
- except ValueError as e:
- assert str(e) == "transactions contains a None value"
-
- try:
- sdk.add_transactions_async(test_value)
- assert False
- except ValueError as e:
- assert str(e) == "transactions contains a None value"
-
-
-def test_parent_tx(sdk):
- id_tx = Transaction(
- amount=24.56,
- description="TARGET T- 5800 20th St 11/30/19 17:32",
- entry_type="debit",
- date="2012-12-10",
- account_holder_id="1",
- iso_currency_code="USD",
- mcc=5432,
- )
- enriched = sdk.add_transactions([id_tx])[0]
- assert enriched.parent_tx is id_tx
-
-
-def test_single_transaction_enrich_error(sdk):
- tx = Transaction(
- amount=24.56,
- description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
- entry_type="debit",
- date="2012-12-10",
- account_holder_type="business",
- iso_currency_code="USD",
- )
- try:
- sdk.add_transactions(tx)
- assert False
- except TypeError as e:
- assert str(e) == "transactions must be either a pandas.Dataframe or an iterable"
-
-
-def test_enriched_fields(sdk):
- tx = Transaction(
- transaction_id="test-enriched-fields",
- amount=12046.15,
- description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
- entry_type="debit",
- date="2019-12-01",
- account_holder_type="business",
- iso_currency_code="USD",
- country="US",
- # mcc=5432,
- )
- df = pd.DataFrame(
- data={
- "amount": [12046.15],
- "description": [
- "AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15"
- ],
- "entry_type": ["debit"],
- "date": ["2019-12-01"],
- "account_holder_type": ["business"],
- "iso_currency_code": ["USD"],
- "transaction_id": ["test-enriched-fields"],
- # "mcc": [5432],
- }
- )
-
- enriched_df = sdk.add_transactions(df).iloc[0]
- enriched_list = sdk.add_transactions([tx])[0]
-
- for enriched in [enriched_df, enriched_list]:
- print(enriched)
- assert "infrastructure" in enriched.labels
- # assert len(enriched.location) > 0
- assert enriched.logo == "https://logos.ntropy.com/aws.amazon.com"
- assert enriched.merchant == "Amazon Web Services"
- assert enriched.merchant_id == str(
- uuid.uuid3(uuid.NAMESPACE_DNS, enriched.website)
- )
- assert enriched.transaction_id == "test-enriched-fields"
- assert enriched.website == "aws.amazon.com"
- # assert enriched.recurrence == "one off"
- assert 0 <= enriched.confidence <= 1
- assert any(enriched.transaction_type == t.value for t in TransactionType)
- # assert 5432 in enriched.mcc
-
-
-def test_sdk_region():
- ah_id = str(uuid.uuid4())
- tx = Transaction(
- amount=24.56,
- description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
- entry_type="debit",
- date="2012-12-10",
- account_holder_id=ah_id,
- account_holder_type="consumer",
- iso_currency_code="USD",
- )
-
- _sdk = SDK(API_KEY)
- assert _sdk.base_url == "https://api.ntropy.com"
- res = _sdk.add_transactions([tx])[0]
- assert res.website is not None
-
- _sdk = SDK(API_KEY, region="us")
- assert _sdk.base_url == "https://api.ntropy.com"
- res = _sdk.add_transactions([tx])[0]
- assert res.website is not None
-
- _sdk = SDK(API_KEY, region="eu")
- assert _sdk.base_url == "https://api.eu.ntropy.com"
- res = _sdk.add_transactions([tx])[0]
- assert res.website is not None
-
- with pytest.raises(ValueError):
- _sdk = SDK(API_KEY, region="atlantida")
-
-
-@pytest.fixture()
-def async_sdk():
- sdk = SDK(API_KEY)
- sdk._make_batch = make_batch
-
- with patch.object(sdk, "MAX_SYNC_BATCH", 0):
- with patch.object(sdk, "MAX_BATCH_SIZE", 1):
- yield sdk
-
-
-@pytest.fixture()
-def sync_sdk():
- sdk = SDK(API_KEY)
- sdk._make_batch = lambda batch_status, results: results
-
- with patch.object(sdk, "MAX_SYNC_BATCH", 999999):
- with patch.object(sdk, "MAX_BATCH_SIZE", 1):
- yield sdk
-
-
-@pytest.fixture()
-def input_tx():
- ah_id = str(uuid.uuid4())
- return Transaction(
- amount=24.56,
- description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
- entry_type="debit",
- date="2012-12-10",
- account_holder_id=ah_id,
- account_holder_type="consumer",
- iso_currency_code="USD",
- )
-
-
-def make_batch(batch_status, results):
- return {
- "status": batch_status,
- "created_at": "2023-01-01T00:00:00.000000+00:00",
- "updated_at": "2023-01-01T00:00:00.000000+00:00",
- "id": "mock-id",
- "progress": 1,
- "total": 1,
- "results": results,
- }
-
-
-class MockResponse:
- def __init__(self, status_code, json_data):
- self.status_code = status_code
- self.json_data = json_data
-
- def json(self):
- return self.json_data
-
- def raise_for_status(self):
- r = Response()
- r.status_code = self.status_code
- return r.raise_for_status()
-
-
-def wrap_response(sdk, wrap_meth, responses):
- """
- Mocks a response for `wrap_meth` iterating through `responses` to obtain return values
- """
- orig = sdk.session.request
- responses = iter(responses)
-
- def fn(meth, *args, **kwargs):
- t = None
- if meth == wrap_meth:
- try:
- t = next(responses)
- except StopIteration:
- pass
-
- if not t:
- return orig(meth, *args, **kwargs)
-
- status_code, batch_status, results = t
- return MockResponse(
- status_code,
- sdk._make_batch(batch_status, results), # noqa
- )
- else:
- return orig(meth, *args, **kwargs)
-
- return fn
-
-
-@pytest.mark.parametrize("batch_status", ["finished", "error"])
-def test_async_batch_with_err_ignore_raise(async_sdk, input_tx, batch_status):
- async_sdk._raise_on_enrichment_error = False
- responses = wrap_response(
- async_sdk,
- "GET",
- [
- (
- 200,
- batch_status,
- [
- {
- "transaction_id": "err",
- "error": "internal_error",
- "error_details": "internal_error",
- }
- ],
- ),
- ],
- )
-
- with patch.object(
- async_sdk.session,
- "request",
- side_effect=responses,
- ) as m:
- res = async_sdk.add_transactions([input_tx] * 2)
- assert m.call_count >= 4
- assert len(res) == 2
- assert res[0].error is not None and "mock-id" in str(res[0].error)
- assert res[1].merchant is not None and res[1].error is None
-
-
-def test_async_batch_request_err_ignore_raise(async_sdk, input_tx):
- async_sdk._raise_on_enrichment_error = False
- responses = wrap_response(
- async_sdk,
- "GET",
- [
- (
- 400,
- "error",
- [
- {
- "transaction_id": "err",
- "error": "internal_error",
- "error_details": "internal_error",
- }
- ],
- ),
- ],
- )
-
- with patch.object(
- async_sdk.session,
- "request",
- side_effect=responses,
- ) as m:
- res = async_sdk.add_transactions([input_tx] * 2)
- assert m.call_count >= 4
- assert len(res) == 2
- assert res[0].error is not None and isinstance(res[0].error, NtropyValueError)
- assert res[1].merchant is not None and res[1].error is None
-
-
-def test_sync_batch_request_err_ignore_raise(sync_sdk, input_tx):
- sync_sdk._raise_on_enrichment_error = False
- responses = wrap_response(
- sync_sdk,
- "POST",
- [
- (
- 400,
- "error",
- [
- {
- "transaction_id": "err",
- "error": "internal_error",
- "error_details": "internal_error",
- }
- ],
- ),
- ],
- )
-
- with patch.object(
- sync_sdk.session,
- "request",
- side_effect=responses,
- ) as m:
- res = sync_sdk.add_transactions([input_tx] * 2)
- assert m.call_count == 2
- assert len(res) == 2
- assert res[0].error is not None and isinstance(res[0].error, NtropyValueError)
- assert res[1].merchant is not None and res[1].error is None
-
-
-@pytest.mark.parametrize("batch_status", ["finished", "error"])
-def test_async_batch_with_err(async_sdk, input_tx, batch_status):
- async_sdk._raise_on_enrichment_error = True
- responses = wrap_response(
- async_sdk,
- "GET",
- [
- (
- 200,
- batch_status,
- [
- {
- "transaction_id": "err",
- "error": "internal_error",
- "error_details": "internal_error",
- }
- ],
- ),
- ],
- )
-
- with patch.object(
- async_sdk.session,
- "request",
- side_effect=responses,
- ) as m:
- with pytest.raises(NtropyBatchError) as be:
- async_sdk.add_transactions([input_tx] * 2)
- assert "mock-id" in str(be.value)
- assert m.call_count == 2
-
-
-def test_async_batch_request_err(async_sdk, input_tx):
- async_sdk._raise_on_enrichment_error = True
- responses = wrap_response(
- async_sdk,
- "GET",
- [
- (
- 400,
- "error",
- [
- {
- "transaction_id": "err",
- "error": "internal_error",
- "error_details": "internal_error",
- }
- ],
- ),
- ],
- )
-
- with patch.object(
- async_sdk.session,
- "request",
- side_effect=responses,
- ) as m:
- with pytest.raises(NtropyValueError):
- async_sdk.add_transactions([input_tx] * 2)
- assert m.call_count == 2
-
-
-def test_sync_batch_request_err(sync_sdk, input_tx):
- sync_sdk._raise_on_enrichment_error = True
- responses = wrap_response(
- sync_sdk,
- "POST",
- [
- (
- 400,
- "error",
- [
- {
- "transaction_id": "err",
- "error": "internal_error",
- "error_details": "internal_error",
- }
- ],
- ),
- ],
- )
-
- with patch.object(
- sync_sdk.session,
- "request",
- side_effect=responses,
- ) as m:
- with pytest.raises(NtropyValueError):
- sync_sdk.add_transactions([input_tx] * 2)
- assert m.call_count == 1
-
-
-def test_mapping(sdk):
- tx = Transaction(
- transaction_id="test-enriched-fields",
- amount=12046.15,
- description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
- entry_type="debit",
- date="2019-12-01",
- account_holder_type="business",
- iso_currency_code="USD",
- country="US",
- mcc=5432,
- )
-
- mapping = {"merchant": "company"}
- mapping_invalid = {"invalid": "test"}
-
- enriched_mapping = sdk.add_transactions([tx], mapping=mapping)
- try:
- enriched_mapping_invalid = sdk.add_transactions([tx], mapping=mapping_invalid)
- except KeyError as e:
- assert "invalid mapping" in str(e)
-
- try:
- enriched_mapping[0].merchant
- except AttributeError as e:
- assert str(e) == "'EnrichedTransaction' object has no attribute 'merchant'"
-
- assert enriched_mapping[0].company == "Amazon Web Services"
+def test_pagination(sdk: SDK):
+ tx_ids = set()
+ it = sdk.v3.transactions.list(limit=2).auto_paginate(page_size=2)
+ for tx in islice(it, 10):
+ tx_ids.add(tx.id)
+ assert len(tx_ids) == 10
diff --git a/tests/test_v3.py b/tests/test_v3.py
deleted file mode 100644
index 27947d4..0000000
--- a/tests/test_v3.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from itertools import islice
-from ntropy_sdk import SDK
-
-
-def test_pagination(sdk: SDK):
- tx_ids = set()
- it = sdk.v3.transactions.list(limit=2).auto_paginate(page_size=2)
- for tx in islice(it, 10):
- tx_ids.add(tx.id)
- assert len(tx_ids) == 10
diff --git a/tests/bank_statement_sample.pdf b/tests/v2/bank_statement_sample.pdf
similarity index 100%
rename from tests/bank_statement_sample.pdf
rename to tests/v2/bank_statement_sample.pdf
diff --git a/tests/conftest.py b/tests/v2/conftest.py
similarity index 100%
rename from tests/conftest.py
rename to tests/v2/conftest.py
diff --git a/tests/test_datasources.py b/tests/v2/test_bank_statements.py
similarity index 96%
rename from tests/test_datasources.py
rename to tests/v2/test_bank_statements.py
index 91a01fb..300b1e8 100644
--- a/tests/test_datasources.py
+++ b/tests/v2/test_bank_statements.py
@@ -2,7 +2,7 @@
import pytest
-from ntropy_sdk.errors import NtropyDatasourceError
+from ntropy_sdk.v2.errors import NtropyDatasourceError
from ntropy_sdk.v2.ntropy_sdk import BankStatementRequest
from ntropy_sdk.utils import AccountHolderType
diff --git a/tests/test_income_check.py b/tests/v2/test_income_check.py
similarity index 99%
rename from tests/test_income_check.py
rename to tests/v2/test_income_check.py
index f5d8340..dd776a6 100644
--- a/tests/test_income_check.py
+++ b/tests/v2/test_income_check.py
@@ -1,6 +1,6 @@
import pytest
import pandas as pd
-from ntropy_sdk.income_check import (
+from ntropy_sdk.v2.income_check import (
UNDETERMINED_LABEL,
IncomeGroup,
IncomeLabelEnum,
diff --git a/tests/test_recurring_payments.py b/tests/v2/test_recurring_payments.py
similarity index 98%
rename from tests/test_recurring_payments.py
rename to tests/v2/test_recurring_payments.py
index d8a7b81..e5a1cc0 100644
--- a/tests/test_recurring_payments.py
+++ b/tests/v2/test_recurring_payments.py
@@ -1,6 +1,6 @@
import os
import pytest
-from ntropy_sdk import SDK, Transaction
+from ntropy_sdk.v2 import SDK, Transaction
from tests import API_KEY
diff --git a/tests/v2/test_sdk.py b/tests/v2/test_sdk.py
new file mode 100644
index 0000000..7e944d0
--- /dev/null
+++ b/tests/v2/test_sdk.py
@@ -0,0 +1,851 @@
+import os
+import uuid
+from decimal import Decimal
+from unittest.mock import patch
+
+import pandas as pd
+import pytest
+from requests import Response
+
+from ntropy_sdk.v2 import (
+ AccountHolder,
+ Batch,
+ EnrichedTransaction,
+ SDK,
+ Transaction,
+)
+from ntropy_sdk.v2.errors import NtropyValueError, NtropyBatchError
+from ntropy_sdk.utils import TransactionType
+from ntropy_sdk.v2.ntropy_sdk import ACCOUNT_HOLDER_TYPES
+from tests import API_KEY
+
+
+def test_account_holder_type():
+ def create_account_holder(account_holder_type):
+ return AccountHolder(
+ id=str(uuid.uuid4()),
+ type=account_holder_type,
+ industry="fintech",
+ website="ntropy.com",
+ )
+
+ for t in ACCOUNT_HOLDER_TYPES:
+ account_holder = create_account_holder(t)
+ assert account_holder.type == t
+ with pytest.raises(ValueError):
+ create_account_holder("not_valid")
+
+
+def test_get_account_holder(sdk):
+ account_holder = AccountHolder(
+ id=str(uuid.uuid4()),
+ type="business",
+ industry="fintech",
+ website="ntropy.com",
+ )
+
+ sdk.create_account_holder(account_holder)
+ account_holder2 = sdk.get_account_holder(account_holder.id)
+
+ assert isinstance(account_holder2, AccountHolder)
+ assert account_holder2.type == "business"
+ assert account_holder2.industry == "fintech"
+ assert account_holder2.website == "ntropy.com"
+
+
+def test_account_holder_type_or_id(sdk):
+ id_tx = Transaction(
+ amount=24.56,
+ description="TARGET T- 5800 20th St 11/30/19 17:32",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_id="1",
+ iso_currency_code="USD",
+ mcc=5432,
+ )
+ enriched = sdk.add_transactions([id_tx])[0]
+ assert "missing account holder information" not in enriched.labels
+
+ type_tx = Transaction(
+ amount=24.56,
+ description="TARGET T- 5800 20th St 11/30/19 17:32",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_type="business",
+ iso_currency_code="USD",
+ mcc=5432,
+ )
+ enriched = sdk.add_transactions([type_tx])[0]
+ assert "missing account holder information" not in enriched.labels
+
+
+def test_account_holder_type_or_id_pandas(sdk):
+ account_holder = AccountHolder(
+ id=str(uuid.uuid4()), type="business", industry="fintech", website="ntropy.com"
+ )
+ sdk.create_account_holder(account_holder)
+
+ df = pd.DataFrame(
+ data={
+ "amount": [26],
+ "description": ["TARGET T- 5800 20th St 11/30/19 17:32"],
+ "entry_type": ["debit"],
+ "date": ["2012-12-10"],
+ "account_holder_id": [account_holder.id],
+ "iso_currency_code": ["USD"],
+ }
+ )
+ enriched = sdk.add_transactions(df)
+ assert "missing account holder information" not in enriched.labels[0]
+
+ df = pd.DataFrame(
+ {
+ "amount": [27],
+ "description": ["TARGET T- 5800 20th St 11/30/19 17:32"],
+ "entry_type": ["debit"],
+ "date": ["2012-12-10"],
+ "account_holder_type": ["business"],
+ "iso_currency_code": ["USD"],
+ }
+ )
+ enriched = sdk.add_transactions(df)
+ assert "missing account holder information" not in enriched.labels[0]
+
+
+def test_dataframe_inplace(sdk):
+ df = pd.DataFrame(
+ data={
+ "amount": [26],
+ "description": ["TARGET T- 5800 20th St 11/30/19 17:32"],
+ "entry_type": ["debit"],
+ "date": ["2012-12-10"],
+ "account_holder_id": [str(uuid.uuid4())],
+ "account_holder_type": ["consumer"],
+ "iso_currency_code": ["USD"],
+ }
+ )
+ original_columns = df.columns
+ # inplace = False should NOT change the original dataframe
+ enriched = sdk.add_transactions(df, inplace=False)
+ assert list(df.columns) == list(original_columns)
+ assert "goods" in enriched.labels[0]
+
+ # inplace = True should change the original dataframe
+ sdk.add_transactions(df, inplace=True)
+ assert "goods" in df.labels[0]
+
+
+def test_account_holder_type_or_id_iterable(sdk):
+ id_tx = Transaction(
+ amount=24.56,
+ description="TARGET T- 5800 20th St 11/30/19 17:32",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_id="1",
+ iso_currency_code="USD",
+ mcc=5432,
+ )
+ enriched = sdk.add_transactions((t for t in [id_tx]))[0]
+ assert "missing account holder information" not in enriched.labels
+
+ type_tx = Transaction(
+ amount=24.56,
+ description="TARGET T- 5800 20th St 11/30/19 17:32",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_type="business",
+ iso_currency_code="USD",
+ mcc=5432,
+ )
+ enriched = sdk.add_transactions((t for t in [type_tx]))[0]
+ assert "missing account holder information" not in enriched.labels
+
+
+def test_bad_date():
+ def create_tx(date):
+ return Transaction(
+ amount=24.56,
+ description="TARGET T- 5800 20th St 11/30/19 17:32",
+ entry_type="debit",
+ date=date,
+ account_holder_id="1",
+ iso_currency_code="USD",
+ )
+
+ assert isinstance(create_tx("2021-12-13"), Transaction)
+
+ with pytest.raises(ValueError):
+ create_tx("bad date")
+
+ with pytest.raises(ValueError):
+ create_tx("")
+
+ with pytest.raises(ValueError):
+ create_tx(None)
+
+
+def test_fields():
+ tx = Transaction(
+ amount=24.56,
+ description="TARGET T- 5800 20th St 11/30/19 17:32",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_id="1",
+ iso_currency_code="USD",
+ transaction_id="one-two-three",
+ mcc=5432,
+ )
+
+ assert tx.to_dict() == {
+ "amount": 24.56,
+ "description": "TARGET T- 5800 20th St 11/30/19 17:32",
+ "entry_type": "debit",
+ "date": "2012-12-10",
+ "iso_currency_code": "USD",
+ "transaction_id": "one-two-three",
+ "mcc": 5432,
+ "account_holder_id": "1",
+ }
+
+ with pytest.raises(ValueError):
+ tx = Transaction(
+ amount=float("nan"),
+ description="TARGET T- 5800 20th St 11/30/19 17:32",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_id="1",
+ iso_currency_code="USD",
+ transaction_id="one-two-three",
+ mcc=5432,
+ )
+
+
+def test_enrich_huge_batch(sdk):
+ account_holder = AccountHolder(
+ id=str(uuid.uuid4()), type="business", industry="fintech", website="ntropy.com"
+ )
+ sdk.create_account_holder(account_holder)
+
+ tx = Transaction(
+ amount=24.56,
+ description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_id=account_holder.id,
+ iso_currency_code="USD",
+ )
+
+ txs = [tx] * 10
+ sdk.MAX_BATCH_SIZE = 4
+
+ enriched_txs = sdk.add_transactions(txs)
+
+ assert len(enriched_txs) == len(txs)
+
+ for i, enriched_tx in enumerate(enriched_txs):
+ assert isinstance(enriched_tx, EnrichedTransaction)
+ assert enriched_tx.merchant is not None
+ assert enriched_tx.transaction_id == txs[i].transaction_id
+ assert enriched_tx.parent_tx is txs[i]
+
+
+# TODO: temporarily disabled until persistence timing is adjusted for reports
+# def test_report(sdk):
+# account_holder = AccountHolder(
+# id=str(uuid.uuid4()), type="business", industry="fintech", website="ntropy.com"
+# )
+# sdk.create_account_holder(account_holder)
+#
+# tx = Transaction(
+# amount=24.56,
+# description="TARGET T- 5800 20th St 11/30/19 17:32",
+# entry_type="debit",
+# date="2012-12-10",
+# account_holder_id=account_holder.id,
+# iso_currency_code="USD",
+# )
+# enriched_tx = sdk.add_transactions([tx])[0]
+#
+# enriched_tx.create_report(website="ww2.target.com")
+# enriched_tx.create_report(unplanned_kwarg="bar")
+
+
+def test_hierarchy(sdk):
+ for account_holder_type in ["business", "consumer", "unknown"]:
+ h = sdk.get_labels(account_holder_type)
+ assert isinstance(h, dict)
+
+
+def test_transaction_zero_amount():
+ vals = {
+ "description": "foo",
+ "date": "2021-12-12",
+ "entry_type": "debit",
+ "account_holder_id": "1",
+ "country": "US",
+ "iso_currency_code": "USD",
+ }
+
+ Transaction(amount=0, **vals)
+ Transaction(amount=1, **vals)
+
+ with pytest.raises(ValueError):
+ Transaction(amount=-1, **vals)
+
+
+def test_transaction_entry_type():
+ for et in ["incoming", "outgoing", "debit", "credit"]:
+ Transaction(
+ amount=1.0,
+ description="foo",
+ date="2012-12-10",
+ entry_type=et,
+ account_holder_id="bar",
+ iso_currency_code="USD",
+ country="US",
+ )
+
+ with pytest.raises(ValueError):
+ Transaction(
+ amount=1.0,
+ description="foo",
+ date="2012-12-10",
+ entry_type="bar",
+ account_holder_id="bar",
+ iso_currency_code="bla",
+ country="FOO",
+ )
+
+
+def test_readme():
+ readme_file = open(
+ os.path.join(os.path.dirname(__file__), "..", "README.md")
+ ).read()
+ readme_data = readme_file.split("```python")[1].split("```")[0]
+ readme_data = readme_data.replace("YOUR-API-KEY", API_KEY)
+ exec(readme_data, globals())
+
+
+def test_add_transactions_async(sdk):
+ tx = Transaction(
+ amount=24.56,
+ description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_type="business",
+ iso_currency_code="USD",
+ )
+
+ batch = sdk.add_transactions_async([tx])
+ assert batch.batch_id and len(batch.batch_id) > 0
+
+ enriched = batch.wait()
+ assert enriched[0].merchant == "Amazon Web Services"
+ assert enriched[0].parent_tx is tx
+
+
+def test_add_transactions_async_df(sdk):
+ df = pd.DataFrame(
+ data={
+ "amount": [26],
+ "description": ["TARGET T- 5800 20th St 11/30/19 17:32"],
+ "entry_type": ["debit"],
+ "date": ["2012-12-10"],
+ "account_holder_type": ["business"],
+ "iso_currency_code": ["USD"],
+ }
+ )
+ batch = sdk.add_transactions_async(df)
+ enriched = batch.wait()
+ assert enriched[0].merchant == "Target"
+
+
+def test_add_transactions_async_iterable(sdk):
+ tx = Transaction(
+ amount=24.56,
+ description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_type="business",
+ iso_currency_code="USD",
+ )
+
+ batch = sdk.add_transactions_async((t for t in [tx]))
+ assert batch.batch_id and len(batch.batch_id) > 0
+
+ enriched = batch.wait()
+ assert enriched[0].merchant == "Amazon Web Services"
+
+
+def test_batch(sdk):
+ tx = Transaction(
+ amount=24.56,
+ description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_type="business",
+ iso_currency_code="USD",
+ )
+
+ batch = sdk.add_transactions_async([tx] * 10)
+ resp, status = batch.poll()
+
+ if status != "finished":
+ # Might've finished already
+ assert status == "started" and resp["total"] == 10
+
+ batch.wait()
+
+ resp, status = batch.poll()
+ assert status == "finished" and resp[0].merchant == "Amazon Web Services"
+
+ batch = Batch(sdk=sdk, batch_id=batch.batch_id)
+ resp, status = batch.poll()
+ assert status == "finished" and resp[0].merchant == "Amazon Web Services"
+
+
+def test_numerical_support():
+ tx = Transaction(
+ amount=Decimal(24.56),
+ description="TARGET T- 5800 20th St 11/30/19 17:32",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_id="1",
+ iso_currency_code="USD",
+ mcc="5432",
+ )
+
+ assert isinstance(tx.amount, float)
+ assert isinstance(tx.mcc, int)
+
+
+def test_none_in_txns_error(sdk):
+ test_values = [[None]]
+ for test_value in test_values:
+ try:
+ sdk.add_transactions(test_value)
+ assert False
+ except ValueError as e:
+ assert str(e) == "transactions contains a None value"
+
+ try:
+ sdk.add_transactions_async(test_value)
+ assert False
+ except ValueError as e:
+ assert str(e) == "transactions contains a None value"
+
+
+def test_parent_tx(sdk):
+ id_tx = Transaction(
+ amount=24.56,
+ description="TARGET T- 5800 20th St 11/30/19 17:32",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_id="1",
+ iso_currency_code="USD",
+ mcc=5432,
+ )
+ enriched = sdk.add_transactions([id_tx])[0]
+ assert enriched.parent_tx is id_tx
+
+
+def test_single_transaction_enrich_error(sdk):
+ tx = Transaction(
+ amount=24.56,
+ description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_type="business",
+ iso_currency_code="USD",
+ )
+ try:
+ sdk.add_transactions(tx)
+ assert False
+ except TypeError as e:
+ assert str(e) == "transactions must be either a pandas.Dataframe or an iterable"
+
+
+def test_enriched_fields(sdk):
+ tx = Transaction(
+ transaction_id="test-enriched-fields",
+ amount=12046.15,
+ description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
+ entry_type="debit",
+ date="2019-12-01",
+ account_holder_type="business",
+ iso_currency_code="USD",
+ country="US",
+ # mcc=5432,
+ )
+ df = pd.DataFrame(
+ data={
+ "amount": [12046.15],
+ "description": [
+ "AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15"
+ ],
+ "entry_type": ["debit"],
+ "date": ["2019-12-01"],
+ "account_holder_type": ["business"],
+ "iso_currency_code": ["USD"],
+ "transaction_id": ["test-enriched-fields"],
+ # "mcc": [5432],
+ }
+ )
+
+ enriched_df = sdk.add_transactions(df).iloc[0]
+ enriched_list = sdk.add_transactions([tx])[0]
+
+ for enriched in [enriched_df, enriched_list]:
+ print(enriched)
+ assert "infrastructure" in enriched.labels
+ # assert len(enriched.location) > 0
+ assert enriched.logo == "https://logos.ntropy.com/aws.amazon.com"
+ assert enriched.merchant == "Amazon Web Services"
+ assert enriched.merchant_id == str(
+ uuid.uuid3(uuid.NAMESPACE_DNS, enriched.website)
+ )
+ assert enriched.transaction_id == "test-enriched-fields"
+ assert enriched.website == "aws.amazon.com"
+ # assert enriched.recurrence == "one off"
+ assert 0 <= enriched.confidence <= 1
+ assert any(enriched.transaction_type == t.value for t in TransactionType)
+ # assert 5432 in enriched.mcc
+
+
+def test_sdk_region():
+ ah_id = str(uuid.uuid4())
+ tx = Transaction(
+ amount=24.56,
+ description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_id=ah_id,
+ account_holder_type="consumer",
+ iso_currency_code="USD",
+ )
+
+ _sdk = SDK(API_KEY)
+ assert _sdk.base_url == "https://api.ntropy.com"
+ res = _sdk.add_transactions([tx])[0]
+ assert res.website is not None
+
+ _sdk = SDK(API_KEY, region="us")
+ assert _sdk.base_url == "https://api.ntropy.com"
+ res = _sdk.add_transactions([tx])[0]
+ assert res.website is not None
+
+ _sdk = SDK(API_KEY, region="eu")
+ assert _sdk.base_url == "https://api.eu.ntropy.com"
+ res = _sdk.add_transactions([tx])[0]
+ assert res.website is not None
+
+ with pytest.raises(ValueError):
+ _sdk = SDK(API_KEY, region="atlantida")
+
+
+@pytest.fixture()
+def async_sdk():
+ sdk = SDK(API_KEY)
+ sdk._make_batch = make_batch
+
+ with patch.object(sdk, "MAX_SYNC_BATCH", 0):
+ with patch.object(sdk, "MAX_BATCH_SIZE", 1):
+ yield sdk
+
+
+@pytest.fixture()
+def sync_sdk():
+ sdk = SDK(API_KEY)
+ sdk._make_batch = lambda batch_status, results: results
+
+ with patch.object(sdk, "MAX_SYNC_BATCH", 999999):
+ with patch.object(sdk, "MAX_BATCH_SIZE", 1):
+ yield sdk
+
+
+@pytest.fixture()
+def input_tx():
+ ah_id = str(uuid.uuid4())
+ return Transaction(
+ amount=24.56,
+ description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
+ entry_type="debit",
+ date="2012-12-10",
+ account_holder_id=ah_id,
+ account_holder_type="consumer",
+ iso_currency_code="USD",
+ )
+
+
+def make_batch(batch_status, results):
+ return {
+ "status": batch_status,
+ "created_at": "2023-01-01T00:00:00.000000+00:00",
+ "updated_at": "2023-01-01T00:00:00.000000+00:00",
+ "id": "mock-id",
+ "progress": 1,
+ "total": 1,
+ "results": results,
+ }
+
+
+class MockResponse:
+ def __init__(self, status_code, json_data):
+ self.status_code = status_code
+ self.json_data = json_data
+
+ def json(self):
+ return self.json_data
+
+ def raise_for_status(self):
+ r = Response()
+ r.status_code = self.status_code
+ return r.raise_for_status()
+
+
+def wrap_response(sdk, wrap_meth, responses):
+ """
+ Mocks a response for `wrap_meth` iterating through `responses` to obtain return values
+ """
+ orig = sdk.session.request
+ responses = iter(responses)
+
+ def fn(meth, *args, **kwargs):
+ t = None
+ if meth == wrap_meth:
+ try:
+ t = next(responses)
+ except StopIteration:
+ pass
+
+ if not t:
+ return orig(meth, *args, **kwargs)
+
+ status_code, batch_status, results = t
+ return MockResponse(
+ status_code,
+ sdk._make_batch(batch_status, results), # noqa
+ )
+ else:
+ return orig(meth, *args, **kwargs)
+
+ return fn
+
+
+@pytest.mark.parametrize("batch_status", ["finished", "error"])
+def test_async_batch_with_err_ignore_raise(async_sdk, input_tx, batch_status):
+ async_sdk._raise_on_enrichment_error = False
+ responses = wrap_response(
+ async_sdk,
+ "GET",
+ [
+ (
+ 200,
+ batch_status,
+ [
+ {
+ "transaction_id": "err",
+ "error": "internal_error",
+ "error_details": "internal_error",
+ }
+ ],
+ ),
+ ],
+ )
+
+ with patch.object(
+ async_sdk.session,
+ "request",
+ side_effect=responses,
+ ) as m:
+ res = async_sdk.add_transactions([input_tx] * 2)
+ assert m.call_count >= 4
+ assert len(res) == 2
+ assert res[0].error is not None and "mock-id" in str(res[0].error)
+ assert res[1].merchant is not None and res[1].error is None
+
+
+def test_async_batch_request_err_ignore_raise(async_sdk, input_tx):
+ async_sdk._raise_on_enrichment_error = False
+ responses = wrap_response(
+ async_sdk,
+ "GET",
+ [
+ (
+ 400,
+ "error",
+ [
+ {
+ "transaction_id": "err",
+ "error": "internal_error",
+ "error_details": "internal_error",
+ }
+ ],
+ ),
+ ],
+ )
+
+ with patch.object(
+ async_sdk.session,
+ "request",
+ side_effect=responses,
+ ) as m:
+ res = async_sdk.add_transactions([input_tx] * 2)
+ assert m.call_count >= 4
+ assert len(res) == 2
+ assert res[0].error is not None and isinstance(res[0].error, NtropyValueError)
+ assert res[1].merchant is not None and res[1].error is None
+
+
+def test_sync_batch_request_err_ignore_raise(sync_sdk, input_tx):
+ sync_sdk._raise_on_enrichment_error = False
+ responses = wrap_response(
+ sync_sdk,
+ "POST",
+ [
+ (
+ 400,
+ "error",
+ [
+ {
+ "transaction_id": "err",
+ "error": "internal_error",
+ "error_details": "internal_error",
+ }
+ ],
+ ),
+ ],
+ )
+
+ with patch.object(
+ sync_sdk.session,
+ "request",
+ side_effect=responses,
+ ) as m:
+ res = sync_sdk.add_transactions([input_tx] * 2)
+ assert m.call_count == 2
+ assert len(res) == 2
+ assert res[0].error is not None and isinstance(res[0].error, NtropyValueError)
+ assert res[1].merchant is not None and res[1].error is None
+
+
+@pytest.mark.parametrize("batch_status", ["finished", "error"])
+def test_async_batch_with_err(async_sdk, input_tx, batch_status):
+ async_sdk._raise_on_enrichment_error = True
+ responses = wrap_response(
+ async_sdk,
+ "GET",
+ [
+ (
+ 200,
+ batch_status,
+ [
+ {
+ "transaction_id": "err",
+ "error": "internal_error",
+ "error_details": "internal_error",
+ }
+ ],
+ ),
+ ],
+ )
+
+ with patch.object(
+ async_sdk.session,
+ "request",
+ side_effect=responses,
+ ) as m:
+ with pytest.raises(NtropyBatchError) as be:
+ async_sdk.add_transactions([input_tx] * 2)
+ assert "mock-id" in str(be.value)
+ assert m.call_count == 2
+
+
+def test_async_batch_request_err(async_sdk, input_tx):
+ async_sdk._raise_on_enrichment_error = True
+ responses = wrap_response(
+ async_sdk,
+ "GET",
+ [
+ (
+ 400,
+ "error",
+ [
+ {
+ "transaction_id": "err",
+ "error": "internal_error",
+ "error_details": "internal_error",
+ }
+ ],
+ ),
+ ],
+ )
+
+ with patch.object(
+ async_sdk.session,
+ "request",
+ side_effect=responses,
+ ) as m:
+ with pytest.raises(NtropyValueError):
+ async_sdk.add_transactions([input_tx] * 2)
+ assert m.call_count == 2
+
+
+def test_sync_batch_request_err(sync_sdk, input_tx):
+ sync_sdk._raise_on_enrichment_error = True
+ responses = wrap_response(
+ sync_sdk,
+ "POST",
+ [
+ (
+ 400,
+ "error",
+ [
+ {
+ "transaction_id": "err",
+ "error": "internal_error",
+ "error_details": "internal_error",
+ }
+ ],
+ ),
+ ],
+ )
+
+ with patch.object(
+ sync_sdk.session,
+ "request",
+ side_effect=responses,
+ ) as m:
+ with pytest.raises(NtropyValueError):
+ sync_sdk.add_transactions([input_tx] * 2)
+ assert m.call_count == 1
+
+
+def test_mapping(sdk):
+ tx = Transaction(
+ transaction_id="test-enriched-fields",
+ amount=12046.15,
+ description="AMAZON WEB SERVICES AWS.AMAZON.CO WA Ref5543286P25S Crd15",
+ entry_type="debit",
+ date="2019-12-01",
+ account_holder_type="business",
+ iso_currency_code="USD",
+ country="US",
+ mcc=5432,
+ )
+
+ mapping = {"merchant": "company"}
+ mapping_invalid = {"invalid": "test"}
+
+ enriched_mapping = sdk.add_transactions([tx], mapping=mapping)
+ try:
+ enriched_mapping_invalid = sdk.add_transactions([tx], mapping=mapping_invalid)
+ except KeyError as e:
+ assert "invalid mapping" in str(e)
+
+ try:
+ enriched_mapping[0].merchant
+ except AttributeError as e:
+ assert str(e) == "'EnrichedTransaction' object has no attribute 'merchant'"
+
+ assert enriched_mapping[0].company == "Amazon Web Services"
From 487c9d6b718b02359dcb24affc4dc2ea8569cf9e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 12:52:51 +0100
Subject: [PATCH 06/64] remove ipynb
---
tests/test_recurring_payments.ipynb | 293 ----------------------------
1 file changed, 293 deletions(-)
delete mode 100644 tests/test_recurring_payments.ipynb
diff --git a/tests/test_recurring_payments.ipynb b/tests/test_recurring_payments.ipynb
deleted file mode 100644
index 2f726cc..0000000
--- a/tests/test_recurring_payments.ipynb
+++ /dev/null
@@ -1,293 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {
- "pycharm": {
- "name": "#%%\n"
- }
- },
- "outputs": [],
- "source": [
- "%load_ext autoreload\n",
- "%autoreload 2"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 17,
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "env: NTROPY_API_KEY=\n",
- "env: NTROPY_API_URL=\n"
- ]
- }
- ],
- "source": [
- "%env NTROPY_API_KEY=\n",
- "%env NTROPY_API_URL="
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {
- "pycharm": {
- "name": "#%%\n"
- }
- },
- "outputs": [],
- "source": [
- "import os\n",
- "import uuid\n",
- "from IPython.core.display_functions import display\n",
- "from tests import API_KEY\n",
- "from ntropy_sdk import SDK, Transaction\n",
- "\n",
- "sdk = SDK(API_KEY)\n",
- "\n",
- "url = os.environ.get(\"NTROPY_API_URL\")\n",
- "if url is not None:\n",
- " sdk.base_url = url"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {
- "pycharm": {
- "name": "#%%\n"
- }
- },
- "outputs": [],
- "source": [
- "ah_id = str(uuid.uuid4())\n",
- "transactions = [\n",
- " (\"2021-01-01\", 17.99, \"Netflix\",),\n",
- " (\"2021-02-01\", 17.99, \"Netflix\"),\n",
- " (\"2021-03-01\", 17.99, \"Netflix\"),\n",
- " (\"2021-04-01\", 17.99, \"Netflix\"),\n",
- "\n",
- " (\"2021-01-15\", 9.99, \"Spotify\"),\n",
- " (\"2021-02-15\", 9.99, \"Spotify\"),\n",
- " (\"2021-03-15\", 9.99, \"Spotify\"),\n",
- "\n",
- " (\"2021-03-15\", 11.99, \"Dropbox\"),\n",
- "\n",
- " (\"2021-01-01\", 100.0, \"Consolidated Edison\"),\n",
- " (\"2021-02-01\", 100.0, \"Consolidated Edison\"),\n",
- " (\"2021-03-01\", 100.0, \"Consolidated Edison\"),\n",
- "\n",
- " (\"2021-01-01\", 1000.0, 'Rent'),\n",
- " (\"2021-02-01\", 1000.0, 'Rent'),\n",
- " (\"2021-03-01\", 1000.0, 'Rent'),\n",
- " ]\n",
- "\n",
- "transactions = [\n",
- " Transaction(\n",
- " date=tx[0],\n",
- " amount=tx[1],\n",
- " description=tx[2],\n",
- " entry_type=\"debit\",\n",
- " iso_currency_code=\"USD\",\n",
- " transaction_id=f\"tx-{i}\",\n",
- " account_holder_type=\"consumer\",\n",
- " account_holder_id='rec-ah-1',\n",
- " )\n",
- " for i, tx in enumerate(transactions)\n",
- "]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {
- "pycharm": {
- "name": "#%%\n"
- }
- },
- "outputs": [
- {
- "data": {
- "text/plain": "date amount entry_type iso_currency_code description account_holder_id account_holder_type transaction_id labels location logo merchant merchant_id person website chart_of_accounts recurrence recurrence_group\n---------- -------- ------------ ------------------- ------------- ------------------------------------ --------------------- ---------------- ------------------------------------------------ ---------- ---------------------------------------------------- ------------------- ------------------------------------ -------- ----------- ------------------- ------------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n2022-01-01 17.99 debit USD Netflix cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-1 ['entertainment', 'television', 'subscriptions'] https://logos.ntropy.com/netflix.com Netflix dc425051-df94-3509-9103-cf8c0f0bcf6a netflix.com [] subscription {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 4, 1), 'frequency_in_days': 31, 'average_amount_per_tx': 17.99, 'other_party': 'netflix.com', 'id': '1742734f-dca0-3549-b8af-515ebc272098', 'transaction_ids': ['tx-1', 'tx-2', 'tx-3', 'tx-4'], 'periodicity': 'monthly', 'total_amount': 71.96}\n2022-02-01 17.99 debit USD Netflix cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-2 ['entertainment', 'television', 'subscriptions'] https://logos.ntropy.com/netflix.com Netflix dc425051-df94-3509-9103-cf8c0f0bcf6a netflix.com [] subscription {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 4, 1), 'frequency_in_days': 31, 'average_amount_per_tx': 17.99, 'other_party': 'netflix.com', 'id': '1742734f-dca0-3549-b8af-515ebc272098', 'transaction_ids': ['tx-1', 'tx-2', 'tx-3', 'tx-4'], 'periodicity': 'monthly', 'total_amount': 71.96}\n2022-03-01 17.99 debit USD Netflix cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-3 ['entertainment', 'television', 'subscriptions'] https://logos.ntropy.com/netflix.com Netflix dc425051-df94-3509-9103-cf8c0f0bcf6a netflix.com [] subscription {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 4, 1), 'frequency_in_days': 31, 'average_amount_per_tx': 17.99, 'other_party': 'netflix.com', 'id': '1742734f-dca0-3549-b8af-515ebc272098', 'transaction_ids': ['tx-1', 'tx-2', 'tx-3', 'tx-4'], 'periodicity': 'monthly', 'total_amount': 71.96}\n2022-04-01 17.99 debit USD Netflix cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-4 ['entertainment', 'television', 'subscriptions'] https://logos.ntropy.com/netflix.com Netflix dc425051-df94-3509-9103-cf8c0f0bcf6a netflix.com [] subscription {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 4, 1), 'frequency_in_days': 31, 'average_amount_per_tx': 17.99, 'other_party': 'netflix.com', 'id': '1742734f-dca0-3549-b8af-515ebc272098', 'transaction_ids': ['tx-1', 'tx-2', 'tx-3', 'tx-4'], 'periodicity': 'monthly', 'total_amount': 71.96}\n2022-01-15 9.99 debit USD Spotify cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-5 ['entertainment', 'music', 'subscriptions'] https://logos.ntropy.com/spotify.com Spotify dff87b60-eb39-3df9-9232-39f89ff78df9 spotify.com [] subscription {'date_of_first_tx': datetime.date(2022, 1, 15), 'date_of_last_tx': datetime.date(2022, 3, 15), 'frequency_in_days': 29, 'average_amount_per_tx': 9.99, 'other_party': 'spotify.com', 'id': 'e2ec0a07-fd81-3780-b99b-3ca3f7445fbe', 'transaction_ids': ['tx-5', 'tx-6', 'tx-7'], 'periodicity': 'monthly', 'total_amount': 29.97}\n2022-02-15 9.99 debit USD Spotify cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-6 ['entertainment', 'music', 'subscriptions'] https://logos.ntropy.com/spotify.com Spotify dff87b60-eb39-3df9-9232-39f89ff78df9 spotify.com [] subscription {'date_of_first_tx': datetime.date(2022, 1, 15), 'date_of_last_tx': datetime.date(2022, 3, 15), 'frequency_in_days': 29, 'average_amount_per_tx': 9.99, 'other_party': 'spotify.com', 'id': 'e2ec0a07-fd81-3780-b99b-3ca3f7445fbe', 'transaction_ids': ['tx-5', 'tx-6', 'tx-7'], 'periodicity': 'monthly', 'total_amount': 29.97}\n2022-03-15 9.99 debit USD Spotify cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-7 ['entertainment', 'music', 'subscriptions'] https://logos.ntropy.com/spotify.com Spotify dff87b60-eb39-3df9-9232-39f89ff78df9 spotify.com [] subscription {'date_of_first_tx': datetime.date(2022, 1, 15), 'date_of_last_tx': datetime.date(2022, 3, 15), 'frequency_in_days': 29, 'average_amount_per_tx': 9.99, 'other_party': 'spotify.com', 'id': 'e2ec0a07-fd81-3780-b99b-3ca3f7445fbe', 'transaction_ids': ['tx-5', 'tx-6', 'tx-7'], 'periodicity': 'monthly', 'total_amount': 29.97}\n2022-03-15 11.99 debit USD Dropbox cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-8 ['goods', 'software', 'subscriptions'] https://logos.ntropy.com/dropbox.com Dropbox 65799e0f-90c4-3cc0-9ae6-499d81e79137 dropbox.com [] subscription {'date_of_first_tx': datetime.date(2022, 3, 15), 'date_of_last_tx': datetime.date(2022, 3, 15), 'frequency_in_days': 30, 'average_amount_per_tx': 11.99, 'other_party': 'dropbox.com', 'id': 'de657ab1-d4f5-3789-b728-a7a45908908b', 'transaction_ids': ['tx-8'], 'periodicity': 'monthly', 'total_amount': 11.99}\n2022-01-01 1000 debit USD Rent cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-9 ['housing', 'rent'] https://logos.ntropy.com/consumer_icons-housing-rent [] recurring {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 3, 1), 'frequency_in_days': 29, 'average_amount_per_tx': 1000.0, 'other_party': 'unknown', 'id': '9213c470-5a8e-3436-b857-e6f503476e7f', 'transaction_ids': ['tx-9', 'tx-10', 'tx-11'], 'periodicity': 'monthly', 'total_amount': 3000.0}\n2022-02-01 1000 debit USD Rent cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-10 ['housing', 'rent'] https://logos.ntropy.com/consumer_icons-housing-rent [] recurring {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 3, 1), 'frequency_in_days': 29, 'average_amount_per_tx': 1000.0, 'other_party': 'unknown', 'id': '9213c470-5a8e-3436-b857-e6f503476e7f', 'transaction_ids': ['tx-9', 'tx-10', 'tx-11'], 'periodicity': 'monthly', 'total_amount': 3000.0}\n2022-03-01 1000 debit USD Rent cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-11 ['housing', 'rent'] https://logos.ntropy.com/consumer_icons-housing-rent [] recurring {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 3, 1), 'frequency_in_days': 29, 'average_amount_per_tx': 1000.0, 'other_party': 'unknown', 'id': '9213c470-5a8e-3436-b857-e6f503476e7f', 'transaction_ids': ['tx-9', 'tx-10', 'tx-11'], 'periodicity': 'monthly', 'total_amount': 3000.0}\n2022-01-01 100 debit USD Con Edison cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-12 ['utilities'] https://logos.ntropy.com/coned.com Consolidated Edison 53f5eb62-74aa-39de-bd42-edb6cd8b53e1 coned.com [] recurring {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 3, 1), 'frequency_in_days': 29, 'average_amount_per_tx': 100.0, 'other_party': 'coned.com', 'id': 'b0032c36-89d1-346c-87e2-27f93a17df47', 'transaction_ids': ['tx-12', 'tx-13', 'tx-14'], 'periodicity': 'monthly', 'total_amount': 300.0}\n2022-02-01 100 debit USD Con Edison cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-13 ['utilities'] https://logos.ntropy.com/coned.com Consolidated Edison 53f5eb62-74aa-39de-bd42-edb6cd8b53e1 coned.com [] recurring {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 3, 1), 'frequency_in_days': 29, 'average_amount_per_tx': 100.0, 'other_party': 'coned.com', 'id': 'b0032c36-89d1-346c-87e2-27f93a17df47', 'transaction_ids': ['tx-12', 'tx-13', 'tx-14'], 'periodicity': 'monthly', 'total_amount': 300.0}\n2022-03-01 100 debit USD Con Edison cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 consumer tx-14 ['utilities'] https://logos.ntropy.com/coned.com Consolidated Edison 53f5eb62-74aa-39de-bd42-edb6cd8b53e1 coned.com [] recurring {'date_of_first_tx': datetime.date(2022, 1, 1), 'date_of_last_tx': datetime.date(2022, 3, 1), 'frequency_in_days': 29, 'average_amount_per_tx': 100.0, 'other_party': 'coned.com', 'id': 'b0032c36-89d1-346c-87e2-27f93a17df47', 'transaction_ids': ['tx-12', 'tx-13', 'tx-14'], 'periodicity': 'monthly', 'total_amount': 300.0}",
- "text/html": "
\n\n
\n \n \n | \n date | \n amount | \n entry_type | \n iso_currency_code | \n description | \n account_holder_id | \n account_holder_type | \n transaction_id | \n labels | \n location | \n logo | \n merchant | \n merchant_id | \n person | \n website | \n chart_of_accounts | \n recurrence | \n recurrence_group | \n
\n \n \n \n 0 | \n 2022-01-01 | \n 17.99 | \n debit | \n USD | \n Netflix | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-1 | \n [entertainment, television, subscriptions] | \n None | \n https://logos.ntropy.com/netflix.com | \n Netflix | \n dc425051-df94-3509-9103-cf8c0f0bcf6a | \n None | \n netflix.com | \n [] | \n subscription | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n 1 | \n 2022-02-01 | \n 17.99 | \n debit | \n USD | \n Netflix | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-2 | \n [entertainment, television, subscriptions] | \n None | \n https://logos.ntropy.com/netflix.com | \n Netflix | \n dc425051-df94-3509-9103-cf8c0f0bcf6a | \n None | \n netflix.com | \n [] | \n subscription | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n 2 | \n 2022-03-01 | \n 17.99 | \n debit | \n USD | \n Netflix | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-3 | \n [entertainment, television, subscriptions] | \n None | \n https://logos.ntropy.com/netflix.com | \n Netflix | \n dc425051-df94-3509-9103-cf8c0f0bcf6a | \n None | \n netflix.com | \n [] | \n subscription | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n 3 | \n 2022-04-01 | \n 17.99 | \n debit | \n USD | \n Netflix | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-4 | \n [entertainment, television, subscriptions] | \n None | \n https://logos.ntropy.com/netflix.com | \n Netflix | \n dc425051-df94-3509-9103-cf8c0f0bcf6a | \n None | \n netflix.com | \n [] | \n subscription | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n 4 | \n 2022-01-15 | \n 9.99 | \n debit | \n USD | \n Spotify | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-5 | \n [entertainment, music, subscriptions] | \n None | \n https://logos.ntropy.com/spotify.com | \n Spotify | \n dff87b60-eb39-3df9-9232-39f89ff78df9 | \n None | \n spotify.com | \n [] | \n subscription | \n {'date_of_first_tx': 2022-01-15, 'date_of_last... | \n
\n \n 5 | \n 2022-02-15 | \n 9.99 | \n debit | \n USD | \n Spotify | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-6 | \n [entertainment, music, subscriptions] | \n None | \n https://logos.ntropy.com/spotify.com | \n Spotify | \n dff87b60-eb39-3df9-9232-39f89ff78df9 | \n None | \n spotify.com | \n [] | \n subscription | \n {'date_of_first_tx': 2022-01-15, 'date_of_last... | \n
\n \n 6 | \n 2022-03-15 | \n 9.99 | \n debit | \n USD | \n Spotify | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-7 | \n [entertainment, music, subscriptions] | \n None | \n https://logos.ntropy.com/spotify.com | \n Spotify | \n dff87b60-eb39-3df9-9232-39f89ff78df9 | \n None | \n spotify.com | \n [] | \n subscription | \n {'date_of_first_tx': 2022-01-15, 'date_of_last... | \n
\n \n 7 | \n 2022-03-15 | \n 11.99 | \n debit | \n USD | \n Dropbox | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-8 | \n [goods, software, subscriptions] | \n None | \n https://logos.ntropy.com/dropbox.com | \n Dropbox | \n 65799e0f-90c4-3cc0-9ae6-499d81e79137 | \n None | \n dropbox.com | \n [] | \n subscription | \n {'date_of_first_tx': 2022-03-15, 'date_of_last... | \n
\n \n 8 | \n 2022-01-01 | \n 1000.00 | \n debit | \n USD | \n Rent | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-9 | \n [housing, rent] | \n None | \n https://logos.ntropy.com/consumer_icons-housin... | \n None | \n None | \n None | \n None | \n [] | \n recurring | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n 9 | \n 2022-02-01 | \n 1000.00 | \n debit | \n USD | \n Rent | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-10 | \n [housing, rent] | \n None | \n https://logos.ntropy.com/consumer_icons-housin... | \n None | \n None | \n None | \n None | \n [] | \n recurring | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n 10 | \n 2022-03-01 | \n 1000.00 | \n debit | \n USD | \n Rent | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-11 | \n [housing, rent] | \n None | \n https://logos.ntropy.com/consumer_icons-housin... | \n None | \n None | \n None | \n None | \n [] | \n recurring | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n 11 | \n 2022-01-01 | \n 100.00 | \n debit | \n USD | \n Con Edison | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-12 | \n [utilities] | \n None | \n https://logos.ntropy.com/coned.com | \n Consolidated Edison | \n 53f5eb62-74aa-39de-bd42-edb6cd8b53e1 | \n None | \n coned.com | \n [] | \n recurring | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n 12 | \n 2022-02-01 | \n 100.00 | \n debit | \n USD | \n Con Edison | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-13 | \n [utilities] | \n None | \n https://logos.ntropy.com/coned.com | \n Consolidated Edison | \n 53f5eb62-74aa-39de-bd42-edb6cd8b53e1 | \n None | \n coned.com | \n [] | \n recurring | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n 13 | \n 2022-03-01 | \n 100.00 | \n debit | \n USD | \n Con Edison | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n consumer | \n tx-14 | \n [utilities] | \n None | \n https://logos.ntropy.com/coned.com | \n Consolidated Edison | \n 53f5eb62-74aa-39de-bd42-edb6cd8b53e1 | \n None | \n coned.com | \n [] | \n recurring | \n {'date_of_first_tx': 2022-01-01, 'date_of_last... | \n
\n \n
\n
"
- },
- "execution_count": 5,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "sdk.add_transactions(transactions)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 12,
- "outputs": [
- {
- "data": {
- "text/plain": " # txs amount merchant website labels periodicity is_active first_payment_date latest_payment_date next_expected_payment_date type is_essential\n------- -------- ------------ ----------- -------------- ------------- ----------- -------------------- --------------------- ---------------------------- ------------ --------------\n 4 17.99 Netflix netflix.com entertainment monthly True 2022-01-01 2022-04-01 2022-05-01 subscription False\n television\n subscriptions\n 1 11.99 Dropbox dropbox.com goods software monthly True 2022-03-15 2022-03-15 2022-04-15 subscription False\n subscriptions\n 3 9.99 Spotify spotify.com entertainment monthly True 2022-01-15 2022-03-15 2022-04-15 subscription False\n music\n subscriptions\n 3 1000 housing rent monthly True 2022-01-01 2022-03-01 2022-04-01 bill True\n 3 100 Consolidated coned.com utilities monthly True 2022-01-01 2022-03-01 2022-04-01 bill True\n Edison",
- "text/html": "\n\n
\n \n \n | \n # txs | \n amount | \n merchant | \n website | \n labels | \n periodicity | \n is_active | \n first_payment_date | \n latest_payment_date | \n next_expected_payment_date | \n type | \n is_essential | \n
\n \n \n \n 0 | \n 4 | \n 17.99 | \n Netflix | \n netflix.com | \n [entertainment, television, subscriptions] | \n monthly | \n True | \n 2022-01-01 | \n 2022-04-01 | \n 2022-05-01 | \n subscription | \n False | \n
\n \n 1 | \n 1 | \n 11.99 | \n Dropbox | \n dropbox.com | \n [goods, software, subscriptions] | \n monthly | \n True | \n 2022-03-15 | \n 2022-03-15 | \n 2022-04-15 | \n subscription | \n False | \n
\n \n 2 | \n 3 | \n 9.99 | \n Spotify | \n spotify.com | \n [entertainment, music, subscriptions] | \n monthly | \n True | \n 2022-01-15 | \n 2022-03-15 | \n 2022-04-15 | \n subscription | \n False | \n
\n \n 3 | \n 3 | \n 1000.00 | \n | \n | \n [housing, rent] | \n monthly | \n True | \n 2022-01-01 | \n 2022-03-01 | \n 2022-04-01 | \n bill | \n True | \n
\n \n 4 | \n 3 | \n 100.00 | \n Consolidated Edison | \n coned.com | \n [utilities] | \n monthly | \n True | \n 2022-01-01 | \n 2022-03-01 | \n 2022-04-01 | \n bill | \n True | \n
\n \n
\n
"
- },
- "execution_count": 12,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "recurring_payments_groups = sdk.get_recurring_payments(ah_id)\n",
- "recurring_payments_groups"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 13,
- "outputs": [
- {
- "data": {
- "text/plain": "-------------------------- ------------------------------------------------\n# txs 4\namount 17.99\nmerchant Netflix\nwebsite netflix.com\nlabels ['entertainment', 'television', 'subscriptions']\ntype subscription\nis_essential False\nperiodicity monthly\nis_active True\nfirst_payment_date 2022-01-01\nlatest_payment_date 2022-04-01\nnext_expected_payment_date 2022-05-01\n-------------------------- ------------------------------------------------",
- "text/html": "\n\n
\n \n \n | \n key | \n value | \n
\n \n \n \n 0 | \n # txs | \n 4 | \n
\n \n 1 | \n amount | \n 17.99 | \n
\n \n 2 | \n merchant | \n Netflix | \n
\n \n 3 | \n website | \n netflix.com | \n
\n \n 4 | \n labels | \n [entertainment, television, subscriptions] | \n
\n \n 5 | \n type | \n subscription | \n
\n \n 6 | \n is_essential | \n False | \n
\n \n 7 | \n periodicity | \n monthly | \n
\n \n 8 | \n is_active | \n True | \n
\n \n 9 | \n first_payment_date | \n 2022-01-01 | \n
\n \n 10 | \n latest_payment_date | \n 2022-04-01 | \n
\n \n 11 | \n next_expected_payment_date | \n 2022-05-01 | \n
\n \n
\n
"
- },
- "execution_count": 13,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "recurring_payments_groups[0]"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 14,
- "outputs": [
- {
- "data": {
- "text/plain": "date amount entry_type iso_currency_code description account_holder_id transaction_id labels location logo merchant merchant_id person website chart_of_accounts recurrence recurrence_group\n---------- -------- ------------ ------------------- ------------- ------------------------------------ ---------------- ------------------------------------------------ ---------- ------------------------------------ ---------- ------------------------------------ -------- ----------- ------------------- ------------ ------------------\n2022-04-01 17.99 outgoing USD Netflix cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 tx-4 ['entertainment', 'television', 'subscriptions'] https://logos.ntropy.com/netflix.com Netflix dc425051-df94-3509-9103-cf8c0f0bcf6a netflix.com [] subscription\n2022-03-01 17.99 outgoing USD Netflix cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 tx-3 ['entertainment', 'television', 'subscriptions'] https://logos.ntropy.com/netflix.com Netflix dc425051-df94-3509-9103-cf8c0f0bcf6a netflix.com [] subscription\n2022-02-01 17.99 outgoing USD Netflix cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 tx-2 ['entertainment', 'television', 'subscriptions'] https://logos.ntropy.com/netflix.com Netflix dc425051-df94-3509-9103-cf8c0f0bcf6a netflix.com [] subscription\n2022-01-01 17.99 outgoing USD Netflix cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 tx-1 ['entertainment', 'television', 'subscriptions'] https://logos.ntropy.com/netflix.com Netflix dc425051-df94-3509-9103-cf8c0f0bcf6a netflix.com [] subscription",
- "text/html": "\n\n
\n \n \n | \n date | \n amount | \n entry_type | \n iso_currency_code | \n description | \n account_holder_id | \n transaction_id | \n labels | \n location | \n logo | \n merchant | \n merchant_id | \n person | \n website | \n chart_of_accounts | \n recurrence | \n recurrence_group | \n
\n \n \n \n 0 | \n 2022-04-01 | \n 17.99 | \n outgoing | \n USD | \n Netflix | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n tx-4 | \n [entertainment, television, subscriptions] | \n None | \n https://logos.ntropy.com/netflix.com | \n Netflix | \n dc425051-df94-3509-9103-cf8c0f0bcf6a | \n None | \n netflix.com | \n [] | \n subscription | \n None | \n
\n \n 1 | \n 2022-03-01 | \n 17.99 | \n outgoing | \n USD | \n Netflix | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n tx-3 | \n [entertainment, television, subscriptions] | \n None | \n https://logos.ntropy.com/netflix.com | \n Netflix | \n dc425051-df94-3509-9103-cf8c0f0bcf6a | \n None | \n netflix.com | \n [] | \n subscription | \n None | \n
\n \n 2 | \n 2022-02-01 | \n 17.99 | \n outgoing | \n USD | \n Netflix | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n tx-2 | \n [entertainment, television, subscriptions] | \n None | \n https://logos.ntropy.com/netflix.com | \n Netflix | \n dc425051-df94-3509-9103-cf8c0f0bcf6a | \n None | \n netflix.com | \n [] | \n subscription | \n None | \n
\n \n 3 | \n 2022-01-01 | \n 17.99 | \n outgoing | \n USD | \n Netflix | \n cd1738c1-9e66-4d96-b25e-9fc9c7a510f5 | \n tx-1 | \n [entertainment, television, subscriptions] | \n None | \n https://logos.ntropy.com/netflix.com | \n Netflix | \n dc425051-df94-3509-9103-cf8c0f0bcf6a | \n None | \n netflix.com | \n [] | \n subscription | \n None | \n
\n \n
\n
"
- },
- "execution_count": 14,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "recurring_payments_groups[0].transactions"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 15,
- "outputs": [
- {
- "data": {
- "text/plain": " # txs amount merchant website labels periodicity is_active first_payment_date latest_payment_date next_expected_payment_date type is_essential\n------- -------- ---------- ----------- -------------- ------------- ----------- -------------------- --------------------- ---------------------------- ------------ --------------\n 4 17.99 Netflix netflix.com entertainment monthly True 2022-01-01 2022-04-01 2022-05-01 subscription False\n television\n subscriptions\n 1 11.99 Dropbox dropbox.com goods software monthly True 2022-03-15 2022-03-15 2022-04-15 subscription False\n subscriptions\n 3 9.99 Spotify spotify.com entertainment monthly True 2022-01-15 2022-03-15 2022-04-15 subscription False\n music\n subscriptions",
- "text/html": "\n\n
\n \n \n | \n # txs | \n amount | \n merchant | \n website | \n labels | \n periodicity | \n is_active | \n first_payment_date | \n latest_payment_date | \n next_expected_payment_date | \n type | \n is_essential | \n
\n \n \n \n 0 | \n 4 | \n 17.99 | \n Netflix | \n netflix.com | \n [entertainment, television, subscriptions] | \n monthly | \n True | \n 2022-01-01 | \n 2022-04-01 | \n 2022-05-01 | \n subscription | \n False | \n
\n \n 1 | \n 1 | \n 11.99 | \n Dropbox | \n dropbox.com | \n [goods, software, subscriptions] | \n monthly | \n True | \n 2022-03-15 | \n 2022-03-15 | \n 2022-04-15 | \n subscription | \n False | \n
\n \n 2 | \n 3 | \n 9.99 | \n Spotify | \n spotify.com | \n [entertainment, music, subscriptions] | \n monthly | \n True | \n 2022-01-15 | \n 2022-03-15 | \n 2022-04-15 | \n subscription | \n False | \n
\n \n
\n
"
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/plain": "113.92"
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "active_subscriptions = recurring_payments_groups.subscriptions().active()\n",
- "display(active_subscriptions)\n",
- "display(active_subscriptions.total_amount())"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
- },
- {
- "cell_type": "code",
- "execution_count": 16,
- "outputs": [
- {
- "data": {
- "text/plain": " # txs amount merchant website labels periodicity is_active first_payment_date latest_payment_date next_expected_payment_date type is_essential\n------- -------- ------------ --------- ------------ ------------- ----------- -------------------- --------------------- ---------------------------- ------ --------------\n 3 1000 housing rent monthly True 2022-01-01 2022-03-01 2022-04-01 bill True\n 3 100 Consolidated coned.com utilities monthly True 2022-01-01 2022-03-01 2022-04-01 bill True\n Edison",
- "text/html": "\n\n
\n \n \n | \n # txs | \n amount | \n merchant | \n website | \n labels | \n periodicity | \n is_active | \n first_payment_date | \n latest_payment_date | \n next_expected_payment_date | \n type | \n is_essential | \n
\n \n \n \n 0 | \n 3 | \n 1000.0 | \n | \n | \n [housing, rent] | \n monthly | \n True | \n 2022-01-01 | \n 2022-03-01 | \n 2022-04-01 | \n bill | \n True | \n
\n \n 1 | \n 3 | \n 100.0 | \n Consolidated Edison | \n coned.com | \n [utilities] | \n monthly | \n True | \n 2022-01-01 | \n 2022-03-01 | \n 2022-04-01 | \n bill | \n True | \n
\n \n
\n
"
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/plain": "3300.0"
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "active_recurring_bills = recurring_payments_groups.recurring_bills().active()\n",
- "display(active_recurring_bills)\n",
- "display(active_recurring_bills.total_amount())\n"
- ],
- "metadata": {
- "collapsed": false,
- "pycharm": {
- "name": "#%%\n"
- }
- }
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.8.9"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 1
-}
From bdf9362ef69f7c4635c45ea682417c66b102f206 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 12:52:57 +0100
Subject: [PATCH 07/64] fixed imports
---
ntropy_sdk/__init__.py | 18 ++++++++++++++++++
ntropy_sdk/account_holders.py | 4 ++--
ntropy_sdk/bank_statements.py | 8 ++++----
ntropy_sdk/batches.py | 6 +++---
ntropy_sdk/paging.py | 2 +-
ntropy_sdk/sdk.py | 14 ++++++++++++++
ntropy_sdk/transactions.py | 4 ++--
ntropy_sdk/v3/__init__.py | 27 ---------------------------
tests/v2/conftest.py | 2 +-
tests/v3/conftest.py | 17 +++++++++++++++++
tests/{ => v3}/test_sdk.py | 0
11 files changed, 62 insertions(+), 40 deletions(-)
create mode 100644 ntropy_sdk/sdk.py
delete mode 100644 ntropy_sdk/v3/__init__.py
create mode 100644 tests/v3/conftest.py
rename tests/{ => v3}/test_sdk.py (100%)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index b558e4d..03f5a32 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -1,3 +1,21 @@
from .version import VERSION
__version__ = VERSION
+
+from typing import TYPE_CHECKING, Optional
+import requests
+
+
+if TYPE_CHECKING:
+ from ntropy_sdk.v2.ntropy_sdk import SDK
+ from typing_extensions import TypedDict
+
+ class ExtraKwargs(TypedDict, total=False):
+ request_id: Optional[str]
+ api_key: Optional[str]
+ session: Optional[requests.Session]
+
+
+from .sdk import SDK
+
+__all__ = ["SDK"]
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index e6c59a3..6344c36 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -9,8 +9,8 @@
from ntropy_sdk.paging import PagedResponse
if TYPE_CHECKING:
- from ntropy_sdk.v2.ntropy_sdk import SDK
- from ntropy_sdk.v3 import ExtraKwargs
+ from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import SDK
from typing_extensions import Unpack
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 194a1a4..8dcdf67 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -7,14 +7,14 @@
from pydantic import BaseModel, Field, NonNegativeFloat
-from ntropy_sdk.bank_statements import StatementInfo
-from ntropy_sdk.errors import NtropyDatasourceError
+from ntropy_sdk.v2.bank_statements import StatementInfo
+from ntropy_sdk.v2.errors import NtropyDatasourceError
from ntropy_sdk.utils import EntryType
from ntropy_sdk.paging import PagedResponse
if TYPE_CHECKING:
- from ntropy_sdk.v2.ntropy_sdk import SDK
- from ntropy_sdk.v3 import ExtraKwargs
+ from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import SDK
from typing_extensions import Unpack
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index 81d3135..78f4119 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -6,14 +6,14 @@
from pydantic import BaseModel, Field
-from ntropy_sdk.errors import NtropyBatchError
+from ntropy_sdk.v2.errors import NtropyBatchError
from ntropy_sdk.utils import pydantic_json
from ntropy_sdk.paging import PagedResponse
from ntropy_sdk.transactions import EnrichedTransaction, EnrichmentInput
if TYPE_CHECKING:
- from ntropy_sdk.v2.ntropy_sdk import SDK
- from ntropy_sdk.v3 import ExtraKwargs
+ from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import SDK
from typing_extensions import Unpack
diff --git a/ntropy_sdk/paging.py b/ntropy_sdk/paging.py
index 9904890..f330bcf 100644
--- a/ntropy_sdk/paging.py
+++ b/ntropy_sdk/paging.py
@@ -15,7 +15,7 @@
from ntropy_sdk.utils import PYDANTIC_V2
if TYPE_CHECKING:
- from ntropy_sdk.v3 import ExtraKwargs
+ from ntropy_sdk import ExtraKwargs
from pydantic import BaseModel as GenericModel
from typing_extensions import Unpack, Self
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
new file mode 100644
index 0000000..6a6581f
--- /dev/null
+++ b/ntropy_sdk/sdk.py
@@ -0,0 +1,14 @@
+from ntropy_sdk.http import HttpClient
+from ntropy_sdk.transactions import TransactionsResource
+from ntropy_sdk.batches import BatchesResource
+from ntropy_sdk.bank_statements import BankStatementsResource
+from ntropy_sdk.account_holders import AccountHoldersResource
+
+
+class SDK:
+ def __init__(self):
+ self.http_client = HttpClient()
+ self.transactions = TransactionsResource(self)
+ self.batches = BatchesResource(self)
+ self.bank_statements = BankStatementsResource(self)
+ self.account_holders = AccountHoldersResource(self)
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index ba0d135..9d8f435 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -14,8 +14,8 @@
if TYPE_CHECKING:
- from ntropy_sdk.v2.ntropy_sdk import SDK
- from ntropy_sdk.v3 import ExtraKwargs
+ from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import SDK
from typing_extensions import Unpack
diff --git a/ntropy_sdk/v3/__init__.py b/ntropy_sdk/v3/__init__.py
deleted file mode 100644
index cc7812a..0000000
--- a/ntropy_sdk/v3/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from typing import TYPE_CHECKING, Optional
-import requests
-
-
-if TYPE_CHECKING:
- from ntropy_sdk.v2.ntropy_sdk import SDK
- from typing_extensions import TypedDict
-
- class ExtraKwargs(TypedDict, total=False):
- request_id: Optional[str]
- api_key: Optional[str]
- session: Optional[requests.Session]
-
-
-from ntropy_sdk.transactions import TransactionsResource
-from ntropy_sdk.batches import BatchesResource
-from ntropy_sdk.bank_statements import BankStatementsResource
-from ntropy_sdk.account_holders import AccountHoldersResource
-
-
-class V3:
- def __init__(self, sdk: "SDK"):
- self._sdk = sdk
- self.transactions = TransactionsResource(sdk)
- self.batches = BatchesResource(sdk)
- self.bank_statements = BankStatementsResource(sdk)
- self.account_holders = AccountHoldersResource(sdk)
diff --git a/tests/v2/conftest.py b/tests/v2/conftest.py
index b6f1e9e..11fb217 100644
--- a/tests/v2/conftest.py
+++ b/tests/v2/conftest.py
@@ -2,7 +2,7 @@
import pytest as pytest
-from ntropy_sdk import SDK
+from ntropy_sdk.v2 import SDK
from tests import API_KEY
diff --git a/tests/v3/conftest.py b/tests/v3/conftest.py
new file mode 100644
index 0000000..b6f1e9e
--- /dev/null
+++ b/tests/v3/conftest.py
@@ -0,0 +1,17 @@
+import os
+
+import pytest as pytest
+
+from ntropy_sdk import SDK
+from tests import API_KEY
+
+
+@pytest.fixture
+def sdk():
+ sdk = SDK(API_KEY)
+
+ url = os.environ.get("NTROPY_API_URL")
+ if url is not None:
+ sdk.base_url = url
+
+ return sdk
diff --git a/tests/test_sdk.py b/tests/v3/test_sdk.py
similarity index 100%
rename from tests/test_sdk.py
rename to tests/v3/test_sdk.py
From 3d396f945843005958d6d64cea07250be2361a50 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 13:10:09 +0100
Subject: [PATCH 08/64] add more overrides to requests
---
ntropy_sdk/__init__.py | 4 ++++
ntropy_sdk/sdk.py | 25 +++++++++++++++++++++----
2 files changed, 25 insertions(+), 4 deletions(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 03f5a32..149b5aa 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -14,6 +14,10 @@ class ExtraKwargs(TypedDict, total=False):
request_id: Optional[str]
api_key: Optional[str]
session: Optional[requests.Session]
+ retries: Optional[int]
+ timeout: Optional[int]
+ retry_on_unhandled_exception: Optional[int]
+ extra_headers: Optional[int]
from .sdk import SDK
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
index 6a6581f..a9c9c10 100644
--- a/ntropy_sdk/sdk.py
+++ b/ntropy_sdk/sdk.py
@@ -1,14 +1,31 @@
+from typing import TYPE_CHECKING
+
+from ntropy_sdk.account_holders import AccountHoldersResource
+from ntropy_sdk.bank_statements import BankStatementsResource
+from ntropy_sdk.batches import BatchesResource
from ntropy_sdk.http import HttpClient
from ntropy_sdk.transactions import TransactionsResource
-from ntropy_sdk.batches import BatchesResource
-from ntropy_sdk.bank_statements import BankStatementsResource
-from ntropy_sdk.account_holders import AccountHoldersResource
+
+if TYPE_CHECKING:
+ from ntropy_sdk import ExtraKwargs
+ from typing_extensions import Unpack
class SDK:
- def __init__(self):
+ def __init__(self, api_key: str | None = None):
+ self.api_key = api_key
self.http_client = HttpClient()
self.transactions = TransactionsResource(self)
self.batches = BatchesResource(self)
self.bank_statements = BankStatementsResource(self)
self.account_holders = AccountHoldersResource(self)
+
+ def retry_ratelimited_request(
+ self,
+ *args,
+ **kwargs: "Unpack[ExtraKwargs]",
+ ):
+ kwargs_copy = kwargs.copy()
+ if self.api_key and not kwargs_copy.get("api_key"):
+ kwargs_copy["api_key"] = self.api_key
+ return self.http_client.retry_ratelimited_request(*args, **kwargs_copy)
From 0c62fc6469c7793813dd9bba2d4f1adbab25eff0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 13:39:15 +0100
Subject: [PATCH 09/64] cleaned up signatures
---
ntropy_sdk/account_holders.py | 7 +++----
ntropy_sdk/bank_statements.py | 20 ++++++++------------
ntropy_sdk/batches.py | 25 +++++++++++++++++--------
ntropy_sdk/transactions.py | 14 ++++++++------
4 files changed, 36 insertions(+), 30 deletions(-)
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index 6344c36..af9920f 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -74,7 +74,7 @@ def list(
t.request_id = request_id
return page
- def get(self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> AccountHolder:
+ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> AccountHolder:
"""Retrieve an account holder"""
request_id = extra_kwargs.get("request_id")
@@ -90,8 +90,7 @@ def get(self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> AccountHolde
def create(
self,
- *,
- input: AccountHolder,
+ account_holder: AccountHolder,
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> AccountHolder:
"""Create an account holder"""
@@ -103,7 +102,7 @@ def create(
resp = self._sdk.retry_ratelimited_request(
"POST",
"/v3/account_holders",
- payload_json_str=pydantic_json(input),
+ payload_json_str=pydantic_json(account_holder),
**extra_kwargs,
)
return AccountHolder(**resp.json(), request_id=request_id)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 8dcdf67..57a784b 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -38,10 +38,10 @@ class BankStatementJob(BaseModel):
request_id: Optional[str] = None
def is_completed(self):
- ...
+ return self.status == BankStatementJobStatus.COMPLETED
- def is_failed(self):
- ...
+ def is_error(self):
+ return self.status == BankStatementJobStatus.ERROR
def wait_for_results(
self,
@@ -66,7 +66,7 @@ def wait_for_results(
break
time.sleep(poll_interval)
- if self.status is BankStatementJobStatus.COMPLETED:
+ if self.is_completed():
return sdk.v3.bank_statements.results(id=self.id, **extra_kwargs)
else:
raise NtropyDatasourceError()
@@ -148,8 +148,8 @@ def list(
def create(
self,
- *,
file: Union[IOBase, bytes],
+ *,
filename: Optional[str] = None,
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> BankStatementJob:
@@ -168,9 +168,7 @@ def create(
)
return BankStatementJob(**resp.json(), request_id=request_id)
- def get(
- self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
- ) -> BankStatementJob:
+ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BankStatementJob:
request_id = extra_kwargs.get("request_id")
if request_id is None:
request_id = uuid.uuid4().hex
@@ -184,7 +182,7 @@ def get(
return BankStatementJob(**resp.json(), request_id=request_id)
def results(
- self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
+ self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
) -> BankStatementResults:
request_id = extra_kwargs.get("request_id")
if request_id is None:
@@ -198,9 +196,7 @@ def results(
)
return BankStatementResults(**resp.json(), request_id=request_id)
- def overview(
- self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
- ) -> StatementInfo:
+ def overview(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> StatementInfo:
"""Waits for and returns preliminary statement information from the
first page of the PDF. This may not always be consistent with the
final results."""
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index 78f4119..9beb5b8 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -9,7 +9,11 @@
from ntropy_sdk.v2.errors import NtropyBatchError
from ntropy_sdk.utils import pydantic_json
from ntropy_sdk.paging import PagedResponse
-from ntropy_sdk.transactions import EnrichedTransaction, EnrichmentInput
+from ntropy_sdk.transactions import (
+ EnrichedTransaction,
+ EnrichmentInput,
+ InputTransaction,
+)
if TYPE_CHECKING:
from ntropy_sdk import ExtraKwargs
@@ -40,7 +44,13 @@ class Batch(BaseModel):
total: int = Field(description="The total number of transactions in the batch.")
request_id: Optional[str] = None
- def wait(
+ def is_completed(self):
+ return self.status == BatchStatus.COMPLETED
+
+ def is_error(self):
+ return self.status == BatchStatus.ERROR
+
+ def wait_for_results(
self,
sdk: "SDK",
*,
@@ -60,7 +70,7 @@ def wait(
break
time.sleep(poll_interval)
- if self.status is BatchStatus.COMPLETED:
+ if self.is_completed():
return sdk.v3.batches.results(id=self.id, **extra_kwargs)
else:
raise NtropyBatchError(f"Batch[{self.id}] contains errors")
@@ -125,7 +135,7 @@ def list(
t.request_id = request_id
return page
- def get(self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Batch:
+ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Batch:
"""Retrieve a batch"""
request_id = extra_kwargs.get("request_id")
@@ -141,8 +151,7 @@ def get(self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Batch:
def create(
self,
- *,
- input: EnrichmentInput,
+ transactions: List[InputTransaction],
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> Batch:
"""Submit a batch of transactions for enrichment"""
@@ -154,12 +163,12 @@ def create(
resp = self._sdk.retry_ratelimited_request(
"POST",
"/v3/batches",
- payload_json_str=pydantic_json(input),
+ payload_json_str=pydantic_json(EnrichmentInput(transactions=transactions)),
**extra_kwargs,
)
return Batch(**resp.json(), request_id=request_id)
- def results(self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BatchResult:
+ def results(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BatchResult:
request_id = extra_kwargs.get("request_id")
if request_id is None:
request_id = uuid.uuid4().hex
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 9d8f435..d3e7c94 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -344,7 +344,7 @@ def list(
t.request_id = request_id
return page
- def get(self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Transaction:
+ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Transaction:
"""Retrieve a transaction"""
request_id = extra_kwargs.get("request_id")
@@ -361,8 +361,7 @@ def get(self, *, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Transaction:
def create(
self,
- *,
- input: EnrichmentInput,
+ transactions: List[InputTransaction],
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> EnrichmentResult:
"""Synchronously enrich transactions"""
@@ -374,13 +373,16 @@ def create(
resp = self._sdk.retry_ratelimited_request(
"POST",
"/v3/transactions",
- payload_json_str=pydantic_json(input),
+ payload_json_str=pydantic_json(EnrichmentInput(transactions=transactions)),
**extra_kwargs,
)
return EnrichmentResult(**resp.json(), request_id=request_id)
def assign(
- self, *, id: str, account_holder_id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
+ self,
+ transaction_id: str,
+ account_holder_id: str,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
) -> Transaction:
"""Assign a transaction to an account holder"""
@@ -390,7 +392,7 @@ def assign(
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
"POST",
- f"/v3/transactions/{id}/assign",
+ f"/v3/transactions/{transaction_id}/assign",
params={
"account_holder_id": account_holder_id,
},
From de9007791c56900605b469b47b6e5674b55d9cf6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 13:39:46 +0100
Subject: [PATCH 10/64] flake8
---
ntropy_sdk/__init__.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 149b5aa..856a956 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -7,7 +7,6 @@
if TYPE_CHECKING:
- from ntropy_sdk.v2.ntropy_sdk import SDK
from typing_extensions import TypedDict
class ExtraKwargs(TypedDict, total=False):
From 7f940f16a4d8c26d2bfcb76b4fd5e586d9df7922 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 13:48:38 +0100
Subject: [PATCH 11/64] updated errors
---
ntropy_sdk/v2/errors.py | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/ntropy_sdk/v2/errors.py b/ntropy_sdk/v2/errors.py
index 02fbc7f..10d5e91 100644
--- a/ntropy_sdk/v2/errors.py
+++ b/ntropy_sdk/v2/errors.py
@@ -75,18 +75,26 @@ class NtropyValueError(NtropyHTTPError):
class NtropyQuotaExceededError(NtropyHTTPError):
DESCRIPTION = (
- "Reached the transaction limit for this API key. Please contact Ntropy support"
+ "Not enough credits to perform this operation. Please top up your account"
)
class NtropyNotSupportedError(NtropyHTTPError):
- DESCRIPTION = "The requested operation is not support for this API key. Please contact Ntropy support"
+ DESCRIPTION = "The requested operation is not supported for this API key. Please contact Ntropy support"
class NtropyResourceOccupiedError(NtropyHTTPError):
DESCRIPTION = "The resource you're trying to access is busy or not ready yet"
+class NtropyServerConnectionError(NtropyHTTPError):
+ DESCRIPTION = "Server connection error. Please try again later."
+
+
+class NtropyRateLimitError(NtropyHTTPError):
+ DESCRIPTION = "Too many requests. Please try again later."
+
+
ERROR_MAP = {
400: NtropyValueError,
401: NtropyNotAuthorizedError,
@@ -96,6 +104,9 @@ class NtropyResourceOccupiedError(NtropyHTTPError):
422: NtropyValidationError,
423: NtropyQuotaExceededError,
500: NtropyRuntimeError,
+ 502: NtropyServerConnectionError,
+ 503: NtropyServerConnectionError,
+ 504: NtropyServerConnectionError,
}
From 412842acca1aa2dc919adda46217a5fa9b36dddc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 19:29:24 +0100
Subject: [PATCH 12/64] renamed to TransactionInput
---
ntropy_sdk/batches.py | 4 ++--
ntropy_sdk/transactions.py | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index 9beb5b8..8f47f42 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -12,7 +12,7 @@
from ntropy_sdk.transactions import (
EnrichedTransaction,
EnrichmentInput,
- InputTransaction,
+ TransactionInput,
)
if TYPE_CHECKING:
@@ -151,7 +151,7 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Batch:
def create(
self,
- transactions: List[InputTransaction],
+ transactions: List[TransactionInput],
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> Batch:
"""Submit a batch of transactions for enrichment"""
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index d3e7c94..9b790f7 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -62,7 +62,7 @@ class _TransactionBase(BaseModel):
)
-class InputTransaction(_TransactionBase):
+class TransactionInput(_TransactionBase):
account_holder_id: Optional[str] = Field(
None,
description="The id of the account holder. Unsetting it will disable categorization.",
@@ -280,7 +280,7 @@ class EnrichedTransaction(_EnrichedTransactionBase):
class EnrichmentInput(BaseModel):
- transactions: List[InputTransaction]
+ transactions: List[TransactionInput]
class EnrichmentResult(BaseModel):
@@ -361,7 +361,7 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Transaction:
def create(
self,
- transactions: List[InputTransaction],
+ transactions: List[TransactionInput],
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> EnrichmentResult:
"""Synchronously enrich transactions"""
From 912f132d7fce1eec42bbcce21ebaa19f82e69498 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 18 Oct 2024 19:33:07 +0100
Subject: [PATCH 13/64] export errors
---
ntropy_sdk/__init__.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 856a956..a341ec2 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -20,5 +20,4 @@ class ExtraKwargs(TypedDict, total=False):
from .sdk import SDK
-
-__all__ = ["SDK"]
+from .v2.errors import *
From 15dc4cf02b300a3caa4adababfa2726ae2fbcc1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Sun, 20 Oct 2024 10:55:55 +0100
Subject: [PATCH 14/64] imports
---
ntropy_sdk/__init__.py | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index a341ec2..02996f5 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -20,4 +20,20 @@ class ExtraKwargs(TypedDict, total=False):
from .sdk import SDK
-from .v2.errors import *
+from .v2.errors import (
+ NtropyError,
+ NtropyBatchError,
+ NtropyDatasourceError,
+ NtropyTimeoutError,
+ NtropyHTTPError,
+ NtropyValidationError,
+ NtropyQuotaExceededError,
+ NtropyNotSupportedError,
+ NtropyResourceOccupiedError,
+ NtropyServerConnectionError,
+ NtropyRateLimitError,
+ NtropyNotFoundError,
+ NtropyNotAuthorizedError,
+ NtropyValueError,
+ NtropyRuntimeError,
+)
From 25a2c5503a2fb4a10838dc1ccbbfb9b38552d887 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 21 Oct 2024 11:53:24 +0100
Subject: [PATCH 15/64] fixed tests
---
README.md | 31 +++++++++++++++----------------
ntropy_sdk/__init__.py | 6 ++++++
ntropy_sdk/account_holders.py | 14 +++++++-------
ntropy_sdk/bank_statements.py | 20 ++++++++++----------
ntropy_sdk/batches.py | 18 +++++++++---------
ntropy_sdk/http.py | 6 ++++--
ntropy_sdk/sdk.py | 17 ++++++++++++++---
ntropy_sdk/transactions.py | 16 ++++++++--------
ntropy_sdk/v2/__init__.py | 6 ++++--
ntropy_sdk/v2/ntropy_sdk.py | 18 ++++++------------
tests/v2/test_sdk.py | 23 +++++++----------------
tests/v3/test_sdk.py | 13 ++++++++++++-
12 files changed, 102 insertions(+), 86 deletions(-)
diff --git a/README.md b/README.md
index ccbb6c3..a74c8fa 100644
--- a/README.md
+++ b/README.md
@@ -17,24 +17,23 @@ $ python3 -m pip install --upgrade 'ntropy-sdk'
Enriching your first transaction requires an `SDK` object and an input `Transaction` object. The API key can be set in the environment variable `NTROPY_API_KEY` or in the `SDK` constructor:
```python
-from ntropy_sdk import SDK, Transaction
+from ntropy_sdk import SDK, TransactionInput, LocationInput
sdk = SDK("YOUR-API-KEY")
-tx = Transaction(
- description = "AMAZON WEB SERVICES",
- entry_type = "outgoing",
- amount = 12042.37,
- iso_currency_code = "USD",
- date = "2021-11-01",
- transaction_id = "4yp49x3tbj9mD8DB4fM8DDY6Yxbx8YP14g565Xketw3tFmn",
- country = "US",
- account_holder_id = "id-1",
- account_holder_type = "business",
- account_holder_name = "Robin's Tacos",
-)
-
-enriched_tx = sdk.add_transactions([tx])[0]
-print(enriched_tx.merchant)
+r = sdk.transactions.create([
+ TransactionInput(
+ id = "4yp49x3tbj9mD8DB4fM8DDY6Yxbx8YP14g565Xketw3tFmn",
+ description = "AMAZON WEB SERVICES",
+ entry_type = "outgoing",
+ amount = 12042.37,
+ currency = "USD",
+ date = "2021-11-01",
+ location = LocationInput(
+ country="US"
+ ),
+ )
+])
+print(r.transactions[0].entities.counterparty)
```
The returned `EnrichedTransaction` contains the added information by Ntropy API. You can consult the Enrichment section of the documentation for more information on the parameters for both `Transaction` and `EnrichedTransaction`.
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 02996f5..aeea0e8 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -37,3 +37,9 @@ class ExtraKwargs(TypedDict, total=False):
NtropyValueError,
NtropyRuntimeError,
)
+
+from .transactions import (
+ TransactionInput, LocationInput
+)
+
+from .account_holders import AccountHolder
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index af9920f..ab73d40 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -1,6 +1,6 @@
from datetime import datetime
from enum import Enum
-from typing import Optional, TYPE_CHECKING
+from typing import Optional, TYPE_CHECKING, Literal
import uuid
from pydantic import BaseModel, Field
@@ -54,8 +54,8 @@ def list(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- "/v3/account_holders",
+ method="GET",
+ url="/v3/account_holders",
params={
"created_before": created_before,
"created_after": created_after,
@@ -82,8 +82,8 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> AccountHolder:
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/account_holders/{id}",
+ method="GET",
+ url=f"/v3/account_holders/{id}",
**extra_kwargs,
)
return AccountHolder(**resp.json(), request_id=request_id)
@@ -100,8 +100,8 @@ def create(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "POST",
- "/v3/account_holders",
+ method="POST",
+ url="/v3/account_holders",
payload_json_str=pydantic_json(account_holder),
**extra_kwargs,
)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 57a784b..16aea64 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -124,8 +124,8 @@ def list(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- "/v3/bank_statements",
+ method="GET",
+ url="/v3/bank_statements",
params={
"created_before": created_before,
"created_after": created_after,
@@ -158,8 +158,8 @@ def create(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "POST",
- "/v3/bank_statements",
+ method="POST",
+ url="/v3/bank_statements",
payload=None,
files={
"file": file if filename is None else (filename, file),
@@ -174,8 +174,8 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BankStatementJo
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/bank_statements/{id}",
+ method="GET",
+ url=f"/v3/bank_statements/{id}",
payload=None,
**extra_kwargs,
)
@@ -189,8 +189,8 @@ def results(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/bank_statements/{id}/results",
+ method="GET",
+ url=f"/v3/bank_statements/{id}/results",
payload=None,
**extra_kwargs,
)
@@ -205,8 +205,8 @@ def overview(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> StatementI
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/bank_statements/{id}/overview",
+ method="GET",
+ url=f"/v3/bank_statements/{id}/overview",
payload=None,
**extra_kwargs,
)
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index 8f47f42..e32663e 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -6,7 +6,7 @@
from pydantic import BaseModel, Field
-from ntropy_sdk.v2.errors import NtropyBatchError
+from ntropy_sdk.v2 import NtropyBatchError
from ntropy_sdk.utils import pydantic_json
from ntropy_sdk.paging import PagedResponse
from ntropy_sdk.transactions import (
@@ -114,8 +114,8 @@ def list(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- "/v3/batches",
+ method="GET",
+ url="/v3/batches",
params={
"created_before": created_before,
"created_after": created_after,
@@ -143,8 +143,8 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Batch:
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/batches/{id}",
+ method="GET",
+ url=f"/v3/batches/{id}",
**extra_kwargs,
)
return Batch(**resp.json(), request_id=request_id)
@@ -161,8 +161,8 @@ def create(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "POST",
- "/v3/batches",
+ method="POST",
+ url="/v3/batches",
payload_json_str=pydantic_json(EnrichmentInput(transactions=transactions)),
**extra_kwargs,
)
@@ -174,8 +174,8 @@ def results(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BatchResult
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/batches/{id}/results",
+ method="GET",
+ url=f"/v3/batches/{id}/results",
**extra_kwargs,
)
return BatchResult(**resp.json(), request_id=request_id)
diff --git a/ntropy_sdk/http.py b/ntropy_sdk/http.py
index 205f2b4..15c8b75 100644
--- a/ntropy_sdk/http.py
+++ b/ntropy_sdk/http.py
@@ -5,7 +5,6 @@
from typing import Optional
import requests
-from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
from ntropy_sdk import VERSION
from ntropy_sdk.v2.errors import error_from_http_status_code, NtropyError
@@ -18,7 +17,9 @@ def __init__(self, session: requests.Session | None = None):
def _get_session(self) -> requests.Session:
if self._session is None:
self._session = requests.Session()
- self._session.mount("https://", TCPKeepAliveAdapter())
+ # from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
+ # self._session.mount("https://", TCPKeepAliveAdapter())
+ # self._session.mount("https://")
return self._session
@property
@@ -31,6 +32,7 @@ def session(self, session: requests.Session):
def retry_ratelimited_request(
self,
+ *,
method: str,
url: str,
payload: Optional[object] = None,
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
index a9c9c10..25e1ddf 100644
--- a/ntropy_sdk/sdk.py
+++ b/ntropy_sdk/sdk.py
@@ -5,6 +5,7 @@
from ntropy_sdk.batches import BatchesResource
from ntropy_sdk.http import HttpClient
from ntropy_sdk.transactions import TransactionsResource
+from ntropy_sdk.v2.ntropy_sdk import DEFAULT_REGION, ALL_REGIONS
if TYPE_CHECKING:
from ntropy_sdk import ExtraKwargs
@@ -12,7 +13,12 @@
class SDK:
- def __init__(self, api_key: str | None = None):
+ def __init__(
+ self,
+ api_key: str | None = None,
+ region: str = DEFAULT_REGION,
+ ):
+ self.base_url = ALL_REGIONS[region]
self.api_key = api_key
self.http_client = HttpClient()
self.transactions = TransactionsResource(self)
@@ -22,10 +28,15 @@ def __init__(self, api_key: str | None = None):
def retry_ratelimited_request(
self,
- *args,
+ *,
+ method: str,
+ url: str,
**kwargs: "Unpack[ExtraKwargs]",
):
kwargs_copy = kwargs.copy()
if self.api_key and not kwargs_copy.get("api_key"):
kwargs_copy["api_key"] = self.api_key
- return self.http_client.retry_ratelimited_request(*args, **kwargs_copy)
+
+ return self.http_client.retry_ratelimited_request(method=method,
+ url=self.base_url + url,
+ **kwargs_copy)
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 9b790f7..72b77c2 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -321,8 +321,8 @@ def list(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- "/v3/transactions",
+ method="GET",
+ url="/v3/transactions",
params={
"created_before": created_before,
"created_after": created_after,
@@ -352,8 +352,8 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Transaction:
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "GET",
- f"/v3/transactions/{id}",
+ method="GET",
+ url=f"/v3/transactions/{id}",
payload=None,
**extra_kwargs,
)
@@ -371,8 +371,8 @@ def create(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "POST",
- "/v3/transactions",
+ method="POST",
+ url="/v3/transactions",
payload_json_str=pydantic_json(EnrichmentInput(transactions=transactions)),
**extra_kwargs,
)
@@ -391,8 +391,8 @@ def assign(
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- "POST",
- f"/v3/transactions/{transaction_id}/assign",
+ method="POST",
+ url=f"/v3/transactions/{transaction_id}/assign",
params={
"account_holder_id": account_holder_id,
},
diff --git a/ntropy_sdk/v2/__init__.py b/ntropy_sdk/v2/__init__.py
index 6565797..4c634ec 100644
--- a/ntropy_sdk/v2/__init__.py
+++ b/ntropy_sdk/v2/__init__.py
@@ -1,4 +1,4 @@
-from ntropy_sdk.v2.ntropy_sdk import (
+from .ntropy_sdk import (
AccountHolder,
AccountHolderType,
Transaction,
@@ -9,8 +9,9 @@
BankStatement,
BankStatementRequest,
Report,
+ StatementInfo,
)
-from ntropy_sdk.v2.errors import (
+from .errors import (
NtropyError,
NtropyBatchError,
)
@@ -28,4 +29,5 @@
"BankStatement",
"BankStatementRequest",
"Report",
+ "StatementInfo",
)
diff --git a/ntropy_sdk/v2/ntropy_sdk.py b/ntropy_sdk/v2/ntropy_sdk.py
index 8d31d41..347bc9b 100644
--- a/ntropy_sdk/v2/ntropy_sdk.py
+++ b/ntropy_sdk/v2/ntropy_sdk.py
@@ -20,9 +20,8 @@
Union,
)
from itertools import islice
-from json import JSONDecodeError
-import requests # type: ignore
+import requests
from pydantic import (
BaseModel,
Field,
@@ -31,16 +30,14 @@
root_validator,
Extra,
)
-from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter # type: ignore
from tabulate import tabulate
from tqdm.auto import tqdm
-from ntropy_sdk import __version__
-from ntropy_sdk.bank_statements import StatementInfo
-from ntropy_sdk.income_check import IncomeReport, IncomeGroup
+from .bank_statements import StatementInfo
+from .income_check import IncomeReport, IncomeGroup
from ntropy_sdk.http import HttpClient
-from ntropy_sdk.v2.recurring_payments import (
+from .recurring_payments import (
RecurringPaymentsGroups,
RecurringPaymentsGroup,
)
@@ -52,8 +49,7 @@
dict_to_str,
validate_date,
)
-from ntropy_sdk.errors import (
- error_from_http_status_code,
+from .errors import (
NtropyTimeoutError,
NtropyBatchError,
NtropyError,
@@ -64,7 +60,6 @@
NtropyQuotaExceededError,
NtropyValidationError,
)
-from ntropy_sdk.v3 import V3
DEFAULT_TIMEOUT = 10 * 60
DEFAULT_RETRIES = 10
@@ -1266,7 +1261,6 @@ def __init__(
self.token = token
self.http_client = HttpClient()
self.logger = logging.getLogger("Ntropy-SDK")
- self.v3 = V3(self)
self._extra_headers = {}
self._timeout = timeout
@@ -1326,12 +1320,12 @@ def retry_ratelimited_request(
request_id=request_id,
api_key=api_key or self.token,
session=session,
- request_kwargs=request_kwargs,
logger=self.logger,
retries=self._retries,
timeout=self._timeout,
retry_on_unhandled_exception=self._retry_on_unhandled_exception,
extra_headers=self._extra_headers,
+ **request_kwargs,
)
def create_account_holder(self, account_holder: AccountHolder):
diff --git a/tests/v2/test_sdk.py b/tests/v2/test_sdk.py
index 7e944d0..205c928 100644
--- a/tests/v2/test_sdk.py
+++ b/tests/v2/test_sdk.py
@@ -317,15 +317,6 @@ def test_transaction_entry_type():
)
-def test_readme():
- readme_file = open(
- os.path.join(os.path.dirname(__file__), "..", "README.md")
- ).read()
- readme_data = readme_file.split("```python")[1].split("```")[0]
- readme_data = readme_data.replace("YOUR-API-KEY", API_KEY)
- exec(readme_data, globals())
-
-
def test_add_transactions_async(sdk):
tx = Transaction(
amount=24.56,
@@ -607,7 +598,7 @@ def wrap_response(sdk, wrap_meth, responses):
"""
Mocks a response for `wrap_meth` iterating through `responses` to obtain return values
"""
- orig = sdk.session.request
+ orig = sdk.http_client.session.request
responses = iter(responses)
def fn(meth, *args, **kwargs):
@@ -654,7 +645,7 @@ def test_async_batch_with_err_ignore_raise(async_sdk, input_tx, batch_status):
)
with patch.object(
- async_sdk.session,
+ async_sdk.http_client.session,
"request",
side_effect=responses,
) as m:
@@ -686,7 +677,7 @@ def test_async_batch_request_err_ignore_raise(async_sdk, input_tx):
)
with patch.object(
- async_sdk.session,
+ async_sdk.http_client.session,
"request",
side_effect=responses,
) as m:
@@ -718,7 +709,7 @@ def test_sync_batch_request_err_ignore_raise(sync_sdk, input_tx):
)
with patch.object(
- sync_sdk.session,
+ sync_sdk.http_client.session,
"request",
side_effect=responses,
) as m:
@@ -751,7 +742,7 @@ def test_async_batch_with_err(async_sdk, input_tx, batch_status):
)
with patch.object(
- async_sdk.session,
+ async_sdk.http_client.session,
"request",
side_effect=responses,
) as m:
@@ -782,7 +773,7 @@ def test_async_batch_request_err(async_sdk, input_tx):
)
with patch.object(
- async_sdk.session,
+ async_sdk.http_client.session,
"request",
side_effect=responses,
) as m:
@@ -812,7 +803,7 @@ def test_sync_batch_request_err(sync_sdk, input_tx):
)
with patch.object(
- sync_sdk.session,
+ sync_sdk.http_client.session,
"request",
side_effect=responses,
) as m:
diff --git a/tests/v3/test_sdk.py b/tests/v3/test_sdk.py
index 27947d4..664424c 100644
--- a/tests/v3/test_sdk.py
+++ b/tests/v3/test_sdk.py
@@ -1,10 +1,21 @@
+import os
from itertools import islice
from ntropy_sdk import SDK
+from tests import API_KEY
def test_pagination(sdk: SDK):
tx_ids = set()
- it = sdk.v3.transactions.list(limit=2).auto_paginate(page_size=2)
+ it = sdk.transactions.list(limit=2).auto_paginate(page_size=2)
for tx in islice(it, 10):
tx_ids.add(tx.id)
assert len(tx_ids) == 10
+
+
+def test_readme():
+ readme_file = open(
+ os.path.join(os.path.dirname(__file__), "..", "..", "README.md")
+ ).read()
+ readme_data = readme_file.split("```python")[1].split("```")[0]
+ readme_data = readme_data.replace("YOUR-API-KEY", API_KEY)
+ exec(readme_data, globals())
From 002f36cc49e2936d931cabae742015ba81ab850b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 21 Oct 2024 11:57:19 +0100
Subject: [PATCH 16/64] keepalive
---
ntropy_sdk/http.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/ntropy_sdk/http.py b/ntropy_sdk/http.py
index 15c8b75..fc3e5bc 100644
--- a/ntropy_sdk/http.py
+++ b/ntropy_sdk/http.py
@@ -17,9 +17,8 @@ def __init__(self, session: requests.Session | None = None):
def _get_session(self) -> requests.Session:
if self._session is None:
self._session = requests.Session()
- # from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
- # self._session.mount("https://", TCPKeepAliveAdapter())
- # self._session.mount("https://")
+ from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
+ self._session.mount("https://", TCPKeepAliveAdapter())
return self._session
@property
From 5f210dec1d4bf985b512599c2636d7af4b4bdd72 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 21 Oct 2024 13:59:34 +0100
Subject: [PATCH 17/64] typing
---
ntropy_sdk/http.py | 4 ++--
ntropy_sdk/sdk.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/ntropy_sdk/http.py b/ntropy_sdk/http.py
index fc3e5bc..514d0b4 100644
--- a/ntropy_sdk/http.py
+++ b/ntropy_sdk/http.py
@@ -11,7 +11,7 @@
class HttpClient:
- def __init__(self, session: requests.Session | None = None):
+ def __init__(self, session: Optional[requests.Session] = None):
self._session = session
def _get_session(self) -> requests.Session:
@@ -36,7 +36,7 @@ def retry_ratelimited_request(
url: str,
payload: Optional[object] = None,
payload_json_str: Optional[str] = None,
- logger: logging.Logger | None = None,
+ logger: Optional[logging.Logger] = None,
log_level=logging.DEBUG,
request_id: Optional[str] = None,
api_key: Optional[str] = None,
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
index 25e1ddf..eb2ffe9 100644
--- a/ntropy_sdk/sdk.py
+++ b/ntropy_sdk/sdk.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
from ntropy_sdk.account_holders import AccountHoldersResource
from ntropy_sdk.bank_statements import BankStatementsResource
@@ -15,7 +15,7 @@
class SDK:
def __init__(
self,
- api_key: str | None = None,
+ api_key: Optional[str] = None,
region: str = DEFAULT_REGION,
):
self.base_url = ALL_REGIONS[region]
From a7ce64f54faa04a98bda37bdf342ee3358f0d985 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 21 Oct 2024 14:29:14 +0100
Subject: [PATCH 18/64] path
---
tests/v2/conftest.py | 2 +-
tests/v3/conftest.py | 2 +-
tests/v3/test_sdk.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/v2/conftest.py b/tests/v2/conftest.py
index 11fb217..e57075f 100644
--- a/tests/v2/conftest.py
+++ b/tests/v2/conftest.py
@@ -3,7 +3,7 @@
import pytest as pytest
from ntropy_sdk.v2 import SDK
-from tests import API_KEY
+from .. import API_KEY
@pytest.fixture
diff --git a/tests/v3/conftest.py b/tests/v3/conftest.py
index b6f1e9e..da41dcd 100644
--- a/tests/v3/conftest.py
+++ b/tests/v3/conftest.py
@@ -3,7 +3,7 @@
import pytest as pytest
from ntropy_sdk import SDK
-from tests import API_KEY
+from .. import API_KEY
@pytest.fixture
diff --git a/tests/v3/test_sdk.py b/tests/v3/test_sdk.py
index 664424c..af532a5 100644
--- a/tests/v3/test_sdk.py
+++ b/tests/v3/test_sdk.py
@@ -1,7 +1,7 @@
import os
from itertools import islice
from ntropy_sdk import SDK
-from tests import API_KEY
+from .. import API_KEY
def test_pagination(sdk: SDK):
From e1dc3b855e3313e993526e8fa15e36445e7be629 Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Mon, 21 Oct 2024 09:16:28 -0500
Subject: [PATCH 19/64] Add webhooks resource
---
ntropy_sdk/__init__.py | 8 +--
ntropy_sdk/bank_statements.py | 4 +-
ntropy_sdk/http.py | 4 +-
ntropy_sdk/sdk.py | 10 ++-
ntropy_sdk/webhooks.py | 121 ++++++++++++++++++++++++++++++++++
5 files changed, 139 insertions(+), 8 deletions(-)
create mode 100644 ntropy_sdk/webhooks.py
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index aeea0e8..3783a97 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -13,10 +13,10 @@ class ExtraKwargs(TypedDict, total=False):
request_id: Optional[str]
api_key: Optional[str]
session: Optional[requests.Session]
- retries: Optional[int]
- timeout: Optional[int]
- retry_on_unhandled_exception: Optional[int]
- extra_headers: Optional[int]
+ retries: int
+ timeout: int
+ retry_on_unhandled_exception: bool
+ extra_headers: Optional[dict]
from .sdk import SDK
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 16aea64..1b77065 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -61,13 +61,13 @@ def wait_for_results(
]
start_time = time.monotonic()
while time.monotonic() - start_time < timeout:
- self.status = sdk.v3.bank_statements.get(id=self.id).status
+ self.status = sdk.bank_statements.get(id=self.id).status
if self.status in finish_statuses:
break
time.sleep(poll_interval)
if self.is_completed():
- return sdk.v3.bank_statements.results(id=self.id, **extra_kwargs)
+ return sdk.bank_statements.results(id=self.id, **extra_kwargs)
else:
raise NtropyDatasourceError()
diff --git a/ntropy_sdk/http.py b/ntropy_sdk/http.py
index 514d0b4..6df0892 100644
--- a/ntropy_sdk/http.py
+++ b/ntropy_sdk/http.py
@@ -2,7 +2,7 @@
import time
import uuid
from json import JSONDecodeError
-from typing import Optional
+from typing import Dict, Optional
import requests
@@ -34,6 +34,7 @@ def retry_ratelimited_request(
*,
method: str,
url: str,
+ params: Optional[Dict[str, str]] = None,
payload: Optional[object] = None,
payload_json_str: Optional[str] = None,
logger: Optional[logging.Logger] = None,
@@ -89,6 +90,7 @@ def retry_ratelimited_request(
resp = cur_session.request(
method,
url,
+ params=params,
headers=headers,
timeout=timeout,
**request_kwargs,
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
index eb2ffe9..60e0f69 100644
--- a/ntropy_sdk/sdk.py
+++ b/ntropy_sdk/sdk.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Dict, Optional
from ntropy_sdk.account_holders import AccountHoldersResource
from ntropy_sdk.bank_statements import BankStatementsResource
@@ -6,6 +6,7 @@
from ntropy_sdk.http import HttpClient
from ntropy_sdk.transactions import TransactionsResource
from ntropy_sdk.v2.ntropy_sdk import DEFAULT_REGION, ALL_REGIONS
+from ntropy_sdk.webhooks import WebhooksResource
if TYPE_CHECKING:
from ntropy_sdk import ExtraKwargs
@@ -25,12 +26,16 @@ def __init__(
self.batches = BatchesResource(self)
self.bank_statements = BankStatementsResource(self)
self.account_holders = AccountHoldersResource(self)
+ self.webhooks = WebhooksResource(self)
def retry_ratelimited_request(
self,
*,
method: str,
url: str,
+ params: Optional[Dict[str, str]] = None,
+ payload: Optional[object] = None,
+ payload_json_str: Optional[str] = None,
**kwargs: "Unpack[ExtraKwargs]",
):
kwargs_copy = kwargs.copy()
@@ -39,4 +44,7 @@ def retry_ratelimited_request(
return self.http_client.retry_ratelimited_request(method=method,
url=self.base_url + url,
+ params=params,
+ payload=payload,
+ payload_json_str=payload_json_str,
**kwargs_copy)
diff --git a/ntropy_sdk/webhooks.py b/ntropy_sdk/webhooks.py
new file mode 100644
index 0000000..b068cb8
--- /dev/null
+++ b/ntropy_sdk/webhooks.py
@@ -0,0 +1,121 @@
+from datetime import datetime
+from typing import TYPE_CHECKING, List, Literal, Optional
+import uuid
+from pydantic import BaseModel, Field
+
+from ntropy_sdk.paging import PagedResponse
+
+if TYPE_CHECKING:
+ from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import SDK
+ from typing_extensions import Unpack
+
+WebhookEventType = Literal[
+ "reports.resolved",
+ "reports.rejected",
+ "reports.pending",
+ "bank_statements.processing",
+ "bank_statements.processed",
+ "bank_statements.failed",
+ "batches.completed",
+ "batches.error",
+]
+
+
+class Webhook(BaseModel):
+ id: str = Field(
+ description="A generated unique identifier for the webhook",
+ )
+ created_at: datetime = Field(
+ description="The date and time when the webhook was created.",
+ )
+ url: str = Field(
+ description="The URL of the webhook",
+ )
+ events: List[WebhookEventType] = Field(
+ description="A list of events that this webhook subscribes to",
+ )
+ token: Optional[str] = Field(
+ description="A secret string used to authenticate the webhook. This "
+ "value will be included in the `X-Ntropy-Token` header when sending "
+ "requests to the webhook",
+ )
+ request_id: Optional[str] = None
+
+ def delete(self, sdk: "SDK", **extra_kwargs: "Unpack[ExtraKwargs]"):
+ return sdk.webhooks.delete(self.id, **extra_kwargs)
+
+
+class WebhooksResource:
+ def __init__(self, sdk: "SDK"):
+ self._sdk = sdk
+
+ def list(
+ self,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> PagedResponse[Webhook]:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="GET",
+ url="/v3/webhooks",
+ **extra_kwargs,
+ )
+ page = PagedResponse[Webhook](
+ **resp.json(),
+ request_id=request_id,
+ _resource=self,
+ _extra_kwargs=extra_kwargs,
+ )
+ for w in page.data:
+ w.request_id = request_id
+ return page
+
+ def create(
+ self,
+ *,
+ url: str,
+ events: List[WebhookEventType],
+ token: Optional[str],
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> Webhook:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="POST",
+ url="/v3/webhooks",
+ payload={
+ "url": url,
+ "events": events,
+ "token": token,
+ },
+ **extra_kwargs,
+ )
+ return Webhook(**resp.json(), request_id=request_id)
+
+ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Webhook:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="GET",
+ url=f"/v3/webhooks/{id}",
+ **extra_kwargs,
+ )
+ return Webhook(**resp.json(), request_id=request_id)
+
+ def delete(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"):
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="DELETE",
+ url=f"/v3/webhooks/{id}",
+ **extra_kwargs,
+ )
From d609da89b449b6a65afbf878904096b1a41f0919 Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Mon, 21 Oct 2024 14:55:11 -0500
Subject: [PATCH 20/64] Add rules resource
---
ntropy_sdk/rules.py | 96 ++++++++++++++++++++++++++++++++++++++++++
ntropy_sdk/sdk.py | 2 +
ntropy_sdk/webhooks.py | 3 +-
3 files changed, 100 insertions(+), 1 deletion(-)
create mode 100644 ntropy_sdk/rules.py
diff --git a/ntropy_sdk/rules.py b/ntropy_sdk/rules.py
new file mode 100644
index 0000000..5e68d77
--- /dev/null
+++ b/ntropy_sdk/rules.py
@@ -0,0 +1,96 @@
+from typing import TYPE_CHECKING, Any, Dict, List
+import uuid
+
+
+if TYPE_CHECKING:
+ from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import SDK
+ from typing_extensions import Unpack
+
+
+Rule = Dict[str, Any]
+Rules = List[Dict[str, Any]]
+
+
+class RulesResource:
+ def __init__(self, sdk: "SDK"):
+ self._sdk = sdk
+
+ def set(
+ self,
+ rules: Rules,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ):
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="POST",
+ url="/v3/rules",
+ payload=rules,
+ **extra_kwargs,
+ )
+
+ def get(
+ self,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> Rules:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="GET",
+ url="/v3/rules",
+ **extra_kwargs,
+ )
+ return resp.json()
+
+ def append(
+ self,
+ rule: Rule,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ):
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="POST",
+ url="/v3/rules/append",
+ payload=rule,
+ **extra_kwargs,
+ )
+
+ def patch(
+ self,
+ index: int,
+ rule: Rule,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ):
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="PATCH",
+ url=f"/v3/rules/{index}",
+ payload=rule,
+ **extra_kwargs,
+ )
+
+ def delete(
+ self,
+ index: int,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ):
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="DELETE",
+ url=f"/v3/rules/{index}",
+ **extra_kwargs,
+ )
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
index 60e0f69..67158c9 100644
--- a/ntropy_sdk/sdk.py
+++ b/ntropy_sdk/sdk.py
@@ -4,6 +4,7 @@
from ntropy_sdk.bank_statements import BankStatementsResource
from ntropy_sdk.batches import BatchesResource
from ntropy_sdk.http import HttpClient
+from ntropy_sdk.rules import RulesResource
from ntropy_sdk.transactions import TransactionsResource
from ntropy_sdk.v2.ntropy_sdk import DEFAULT_REGION, ALL_REGIONS
from ntropy_sdk.webhooks import WebhooksResource
@@ -27,6 +28,7 @@ def __init__(
self.bank_statements = BankStatementsResource(self)
self.account_holders = AccountHoldersResource(self)
self.webhooks = WebhooksResource(self)
+ self.rules = RulesResource(self)
def retry_ratelimited_request(
self,
diff --git a/ntropy_sdk/webhooks.py b/ntropy_sdk/webhooks.py
index b068cb8..06c8fc0 100644
--- a/ntropy_sdk/webhooks.py
+++ b/ntropy_sdk/webhooks.py
@@ -1,6 +1,7 @@
from datetime import datetime
-from typing import TYPE_CHECKING, List, Literal, Optional
+from typing import List, Literal, Optional, TYPE_CHECKING
import uuid
+
from pydantic import BaseModel, Field
from ntropy_sdk.paging import PagedResponse
From 70c3b85460a76c6d400f3da228ab2fca74479dc9 Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Mon, 21 Oct 2024 15:51:26 -0500
Subject: [PATCH 21/64] Add sdk.webhooks.patch
---
ntropy_sdk/webhooks.py | 43 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 42 insertions(+), 1 deletion(-)
diff --git a/ntropy_sdk/webhooks.py b/ntropy_sdk/webhooks.py
index 06c8fc0..542bc5e 100644
--- a/ntropy_sdk/webhooks.py
+++ b/ntropy_sdk/webhooks.py
@@ -1,5 +1,5 @@
from datetime import datetime
-from typing import List, Literal, Optional, TYPE_CHECKING
+from typing import List, Literal, Optional, TYPE_CHECKING, Union
import uuid
from pydantic import BaseModel, Field
@@ -11,6 +11,13 @@
from ntropy_sdk import SDK
from typing_extensions import Unpack
+
+class _Unset:
+ pass
+
+
+UNSET = _Unset()
+
WebhookEventType = Literal[
"reports.resolved",
"reports.rejected",
@@ -41,6 +48,9 @@ class Webhook(BaseModel):
"value will be included in the `X-Ntropy-Token` header when sending "
"requests to the webhook",
)
+ enabled: bool = Field(
+ description="Whether the webhook is enabled or not.",
+ )
request_id: Optional[str] = None
def delete(self, sdk: "SDK", **extra_kwargs: "Unpack[ExtraKwargs]"):
@@ -120,3 +130,34 @@ def delete(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"):
url=f"/v3/webhooks/{id}",
**extra_kwargs,
)
+
+ def patch(
+ self,
+ id: str,
+ *,
+ url: Union[str, _Unset] = UNSET,
+ events: Union[List[WebhookEventType], _Unset] = UNSET,
+ token: Union[str, None, _Unset] = UNSET,
+ enabled: Union[bool, _Unset] = UNSET,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ):
+ payload = {}
+ if url is not UNSET:
+ payload["url"] = url
+ if events is not UNSET:
+ payload["events"] = events
+ if token is not UNSET:
+ payload["token"] = token
+ if enabled is not UNSET:
+ payload["enabled"] = enabled
+
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="PATCH",
+ url=f"/v3/webhooks/{id}",
+ payload=payload,
+ **extra_kwargs,
+ )
From 44392c073312aa6eed731e155283eb1f7e1656ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 21 Oct 2024 23:19:06 +0100
Subject: [PATCH 22/64] api_key fixture
---
ntropy_sdk/__init__.py | 4 +---
ntropy_sdk/http.py | 1 +
ntropy_sdk/sdk.py | 14 ++++++++------
tests/__init__.py | 8 --------
tests/conftest.py | 13 +++++++++++++
tests/v2/conftest.py | 5 ++---
tests/v2/test_recurring_payments.py | 12 ------------
tests/v2/{test_sdk.py => test_v2_sdk.py} | 19 +++++++++----------
tests/v3/conftest.py | 5 ++---
tests/v3/test_sdk.py | 5 ++---
10 files changed, 38 insertions(+), 48 deletions(-)
delete mode 100644 tests/__init__.py
create mode 100644 tests/conftest.py
rename tests/v2/{test_sdk.py => test_v2_sdk.py} (98%)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 3783a97..d7b7dbf 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -38,8 +38,6 @@ class ExtraKwargs(TypedDict, total=False):
NtropyRuntimeError,
)
-from .transactions import (
- TransactionInput, LocationInput
-)
+from .transactions import TransactionInput, LocationInput
from .account_holders import AccountHolder
diff --git a/ntropy_sdk/http.py b/ntropy_sdk/http.py
index 6df0892..bc7e7e5 100644
--- a/ntropy_sdk/http.py
+++ b/ntropy_sdk/http.py
@@ -18,6 +18,7 @@ def _get_session(self) -> requests.Session:
if self._session is None:
self._session = requests.Session()
from requests_toolbelt.adapters.socket_options import TCPKeepAliveAdapter
+
self._session.mount("https://", TCPKeepAliveAdapter())
return self._session
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
index 67158c9..f0c157d 100644
--- a/ntropy_sdk/sdk.py
+++ b/ntropy_sdk/sdk.py
@@ -44,9 +44,11 @@ def retry_ratelimited_request(
if self.api_key and not kwargs_copy.get("api_key"):
kwargs_copy["api_key"] = self.api_key
- return self.http_client.retry_ratelimited_request(method=method,
- url=self.base_url + url,
- params=params,
- payload=payload,
- payload_json_str=payload_json_str,
- **kwargs_copy)
+ return self.http_client.retry_ratelimited_request(
+ method=method,
+ url=self.base_url + url,
+ params=params,
+ payload=payload,
+ payload_json_str=payload_json_str,
+ **kwargs_copy,
+ )
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index 197d0e6..0000000
--- a/tests/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import os
-
-
-API_KEY = os.environ.get("NTROPY_API_KEY")
-
-
-if not API_KEY:
- raise RuntimeError("Environment variable NTROPY_API_KEY is not defined")
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..d6afd8f
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,13 @@
+import os
+
+import pytest
+
+
+@pytest.fixture()
+def api_key():
+ key = os.environ.get("NTROPY_API_KEY")
+
+ if not key:
+ raise RuntimeError("Environment variable NTROPY_API_KEY is not defined")
+
+ return key
diff --git a/tests/v2/conftest.py b/tests/v2/conftest.py
index e57075f..881a949 100644
--- a/tests/v2/conftest.py
+++ b/tests/v2/conftest.py
@@ -3,12 +3,11 @@
import pytest as pytest
from ntropy_sdk.v2 import SDK
-from .. import API_KEY
@pytest.fixture
-def sdk():
- sdk = SDK(API_KEY)
+def sdk(api_key):
+ sdk = SDK(api_key)
url = os.environ.get("NTROPY_API_URL")
if url is not None:
diff --git a/tests/v2/test_recurring_payments.py b/tests/v2/test_recurring_payments.py
index e5a1cc0..11ce374 100644
--- a/tests/v2/test_recurring_payments.py
+++ b/tests/v2/test_recurring_payments.py
@@ -1,18 +1,6 @@
import os
import pytest
from ntropy_sdk.v2 import SDK, Transaction
-from tests import API_KEY
-
-
-@pytest.fixture
-def sdk():
- sdk = SDK(API_KEY)
-
- url = os.environ.get("NTROPY_API_URL")
- if url is not None:
- sdk.base_url = url
-
- return sdk
@pytest.fixture
diff --git a/tests/v2/test_sdk.py b/tests/v2/test_v2_sdk.py
similarity index 98%
rename from tests/v2/test_sdk.py
rename to tests/v2/test_v2_sdk.py
index 205c928..af0893c 100644
--- a/tests/v2/test_sdk.py
+++ b/tests/v2/test_v2_sdk.py
@@ -17,7 +17,6 @@
from ntropy_sdk.v2.errors import NtropyValueError, NtropyBatchError
from ntropy_sdk.utils import TransactionType
from ntropy_sdk.v2.ntropy_sdk import ACCOUNT_HOLDER_TYPES
-from tests import API_KEY
def test_account_holder_type():
@@ -503,7 +502,7 @@ def test_enriched_fields(sdk):
# assert 5432 in enriched.mcc
-def test_sdk_region():
+def test_sdk_region(api_key):
ah_id = str(uuid.uuid4())
tx = Transaction(
amount=24.56,
@@ -515,28 +514,28 @@ def test_sdk_region():
iso_currency_code="USD",
)
- _sdk = SDK(API_KEY)
+ _sdk = SDK(api_key)
assert _sdk.base_url == "https://api.ntropy.com"
res = _sdk.add_transactions([tx])[0]
assert res.website is not None
- _sdk = SDK(API_KEY, region="us")
+ _sdk = SDK(api_key, region="us")
assert _sdk.base_url == "https://api.ntropy.com"
res = _sdk.add_transactions([tx])[0]
assert res.website is not None
- _sdk = SDK(API_KEY, region="eu")
+ _sdk = SDK(api_key, region="eu")
assert _sdk.base_url == "https://api.eu.ntropy.com"
res = _sdk.add_transactions([tx])[0]
assert res.website is not None
with pytest.raises(ValueError):
- _sdk = SDK(API_KEY, region="atlantida")
+ _sdk = SDK(api_key, region="atlantida")
@pytest.fixture()
-def async_sdk():
- sdk = SDK(API_KEY)
+def async_sdk(api_key):
+ sdk = SDK(api_key)
sdk._make_batch = make_batch
with patch.object(sdk, "MAX_SYNC_BATCH", 0):
@@ -545,8 +544,8 @@ def async_sdk():
@pytest.fixture()
-def sync_sdk():
- sdk = SDK(API_KEY)
+def sync_sdk(api_key):
+ sdk = SDK(api_key)
sdk._make_batch = lambda batch_status, results: results
with patch.object(sdk, "MAX_SYNC_BATCH", 999999):
diff --git a/tests/v3/conftest.py b/tests/v3/conftest.py
index da41dcd..f4fd923 100644
--- a/tests/v3/conftest.py
+++ b/tests/v3/conftest.py
@@ -3,12 +3,11 @@
import pytest as pytest
from ntropy_sdk import SDK
-from .. import API_KEY
@pytest.fixture
-def sdk():
- sdk = SDK(API_KEY)
+def sdk(api_key):
+ sdk = SDK(api_key)
url = os.environ.get("NTROPY_API_URL")
if url is not None:
diff --git a/tests/v3/test_sdk.py b/tests/v3/test_sdk.py
index af532a5..37ff327 100644
--- a/tests/v3/test_sdk.py
+++ b/tests/v3/test_sdk.py
@@ -1,7 +1,6 @@
import os
from itertools import islice
from ntropy_sdk import SDK
-from .. import API_KEY
def test_pagination(sdk: SDK):
@@ -12,10 +11,10 @@ def test_pagination(sdk: SDK):
assert len(tx_ids) == 10
-def test_readme():
+def test_readme(api_key):
readme_file = open(
os.path.join(os.path.dirname(__file__), "..", "..", "README.md")
).read()
readme_data = readme_file.split("```python")[1].split("```")[0]
- readme_data = readme_data.replace("YOUR-API-KEY", API_KEY)
+ readme_data = readme_data.replace("YOUR-API-KEY", api_key)
exec(readme_data, globals())
From 29dc02e212e0039eb78aafc8159e10c1ee061793 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 21 Oct 2024 23:54:40 +0100
Subject: [PATCH 23/64] recurring groups
---
ntropy_sdk/account_holders.py | 25 +++++++++++++++++----
ntropy_sdk/transactions.py | 5 +++++
tests/v3/test_sdk.py | 42 ++++++++++++++++++++++++++++++++++-
3 files changed, 67 insertions(+), 5 deletions(-)
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index ab73d40..231c0fb 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -1,12 +1,13 @@
+import uuid
from datetime import datetime
from enum import Enum
-from typing import Optional, TYPE_CHECKING, Literal
-import uuid
+from typing import Optional, TYPE_CHECKING
from pydantic import BaseModel, Field
-from ntropy_sdk.utils import pydantic_json
from ntropy_sdk.paging import PagedResponse
+from ntropy_sdk.transactions import RecurrenceGroup, RecurrenceGroups
+from ntropy_sdk.utils import pydantic_json
if TYPE_CHECKING:
from ntropy_sdk import ExtraKwargs
@@ -107,4 +108,20 @@ def create(
)
return AccountHolder(**resp.json(), request_id=request_id)
- # TODO: Recurring groups
+ def recurring_groups(
+ self,
+ id: str,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> RecurrenceGroups:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="POST",
+ url=f"/v3/account_holders/{id}/recurring_groups",
+ **extra_kwargs,
+ )
+ return RecurrenceGroups(
+ groups=[RecurrenceGroup(**r) for r in resp.json()], request_id=request_id
+ )
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 72b77c2..b74a99e 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -237,6 +237,11 @@ class RecurrenceGroup(BaseModel):
)
+class RecurrenceGroups(BaseModel):
+ groups: List[RecurrenceGroup]
+ request_id: Optional[str]
+
+
class Recurrence(BaseModel):
"""
The `Recurrence` object represents the recurrence pattern of a transaction. It provides information about
diff --git a/tests/v3/test_sdk.py b/tests/v3/test_sdk.py
index 37ff327..141547c 100644
--- a/tests/v3/test_sdk.py
+++ b/tests/v3/test_sdk.py
@@ -1,6 +1,12 @@
import os
from itertools import islice
-from ntropy_sdk import SDK
+
+from ntropy_sdk import (
+ SDK,
+ TransactionInput,
+ AccountHolder,
+ NtropyValueError,
+)
def test_pagination(sdk: SDK):
@@ -18,3 +24,37 @@ def test_readme(api_key):
readme_data = readme_file.split("```python")[1].split("```")[0]
readme_data = readme_data.replace("YOUR-API-KEY", api_key)
exec(readme_data, globals())
+
+
+def test_recurrence_groups(sdk):
+ try:
+ sdk.account_holders.create(
+ AccountHolder(
+ id="Xksd9SWd",
+ type="consumer",
+ )
+ )
+ except NtropyValueError:
+ pass
+
+ txs = []
+ txs.extend(
+ [
+ TransactionInput(
+ id=f"netflix-{i}",
+ description=f"Recurring Debit Purchase Card 1350 #{i} netflix.com Netflix.com CA",
+ amount=17.99,
+ currency="USD",
+ entry_type="outgoing",
+ date=f"2021-0{i}-01",
+ account_holder_id="Xksd9SWd",
+ )
+ for i in range(1, 5)
+ ]
+ )
+
+ sdk.transactions.create(txs)
+ recurring_groups = sdk.account_holders.recurring_groups("Xksd9SWd")
+
+ assert recurring_groups.groups[0].counterparty.website == "netflix.com"
+ assert recurring_groups.groups[0].periodicity == "monthly"
From 453ebfb27f6d95b85ee708c402a573990a4e3534 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 22 Oct 2024 11:46:27 +0100
Subject: [PATCH 24/64] rename create
---
ntropy_sdk/rules.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ntropy_sdk/rules.py b/ntropy_sdk/rules.py
index 5e68d77..6cc6f7b 100644
--- a/ntropy_sdk/rules.py
+++ b/ntropy_sdk/rules.py
@@ -16,7 +16,7 @@ class RulesResource:
def __init__(self, sdk: "SDK"):
self._sdk = sdk
- def set(
+ def create(
self,
rules: Rules,
**extra_kwargs: "Unpack[ExtraKwargs]",
From c628f6e54e0e288ceb6e561c228ad3830846052c Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Tue, 22 Oct 2024 12:59:23 -0500
Subject: [PATCH 25/64] Use request id from response
---
ntropy_sdk/account_holders.py | 13 +++++++++----
ntropy_sdk/bank_statements.py | 18 +++++++++++++-----
ntropy_sdk/batches.py | 14 ++++++++++----
ntropy_sdk/rules.py | 12 ++++++------
ntropy_sdk/transactions.py | 14 ++++++++++----
ntropy_sdk/v2/ntropy_sdk.py | 6 +++---
ntropy_sdk/webhooks.py | 10 +++++++---
7 files changed, 58 insertions(+), 29 deletions(-)
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index 231c0fb..15d188c 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -67,7 +67,7 @@ def list(
)
page = PagedResponse[AccountHolder](
**resp.json(),
- request_id=request_id,
+ request_id=resp.headers.get("x-request-id", request_id),
_resource=self,
_extra_kwargs=extra_kwargs,
)
@@ -87,7 +87,9 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> AccountHolder:
url=f"/v3/account_holders/{id}",
**extra_kwargs,
)
- return AccountHolder(**resp.json(), request_id=request_id)
+ return AccountHolder(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def create(
self,
@@ -106,7 +108,9 @@ def create(
payload_json_str=pydantic_json(account_holder),
**extra_kwargs,
)
- return AccountHolder(**resp.json(), request_id=request_id)
+ return AccountHolder(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def recurring_groups(
self,
@@ -123,5 +127,6 @@ def recurring_groups(
**extra_kwargs,
)
return RecurrenceGroups(
- groups=[RecurrenceGroup(**r) for r in resp.json()], request_id=request_id
+ groups=[RecurrenceGroup(**r) for r in resp.json()],
+ request_id=resp.headers.get("x-request-id", request_id),
)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 1b77065..10f617b 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -138,7 +138,7 @@ def list(
)
page = PagedResponse[BankStatementJob](
**resp.json(),
- request_id=request_id,
+ request_id=resp.headers.get("x-request-id", request_id),
_resource=self,
_extra_kwargs=extra_kwargs,
)
@@ -166,7 +166,9 @@ def create(
},
**extra_kwargs,
)
- return BankStatementJob(**resp.json(), request_id=request_id)
+ return BankStatementJob(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BankStatementJob:
request_id = extra_kwargs.get("request_id")
@@ -179,7 +181,9 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BankStatementJo
payload=None,
**extra_kwargs,
)
- return BankStatementJob(**resp.json(), request_id=request_id)
+ return BankStatementJob(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def results(
self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
@@ -194,7 +198,9 @@ def results(
payload=None,
**extra_kwargs,
)
- return BankStatementResults(**resp.json(), request_id=request_id)
+ return BankStatementResults(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def overview(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> StatementInfo:
"""Waits for and returns preliminary statement information from the
@@ -210,4 +216,6 @@ def overview(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> StatementI
payload=None,
**extra_kwargs,
)
- return StatementInfo(**resp.json(), request_id=request_id)
+ return StatementInfo(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index e32663e..ef21f5e 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -127,7 +127,7 @@ def list(
)
page = PagedResponse[Batch](
**resp.json(),
- request_id=request_id,
+ request_id=resp.headers.get("x-request-id", request_id),
_resource=self,
_extra_kwargs=extra_kwargs,
)
@@ -147,7 +147,9 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Batch:
url=f"/v3/batches/{id}",
**extra_kwargs,
)
- return Batch(**resp.json(), request_id=request_id)
+ return Batch(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def create(
self,
@@ -166,7 +168,9 @@ def create(
payload_json_str=pydantic_json(EnrichmentInput(transactions=transactions)),
**extra_kwargs,
)
- return Batch(**resp.json(), request_id=request_id)
+ return Batch(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def results(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BatchResult:
request_id = extra_kwargs.get("request_id")
@@ -178,4 +182,6 @@ def results(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BatchResult
url=f"/v3/batches/{id}/results",
**extra_kwargs,
)
- return BatchResult(**resp.json(), request_id=request_id)
+ return BatchResult(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
diff --git a/ntropy_sdk/rules.py b/ntropy_sdk/rules.py
index 6cc6f7b..a5940ae 100644
--- a/ntropy_sdk/rules.py
+++ b/ntropy_sdk/rules.py
@@ -18,7 +18,7 @@ def __init__(self, sdk: "SDK"):
def create(
self,
- rules: Rules,
+ rule: Rule,
**extra_kwargs: "Unpack[ExtraKwargs]",
):
request_id = extra_kwargs.get("request_id")
@@ -28,7 +28,7 @@ def create(
self._sdk.retry_ratelimited_request(
method="POST",
url="/v3/rules",
- payload=rules,
+ payload=rule,
**extra_kwargs,
)
@@ -47,9 +47,9 @@ def get(
)
return resp.json()
- def append(
+ def replace(
self,
- rule: Rule,
+ rules: Rules,
**extra_kwargs: "Unpack[ExtraKwargs]",
):
request_id = extra_kwargs.get("request_id")
@@ -58,8 +58,8 @@ def append(
extra_kwargs["request_id"] = request_id
self._sdk.retry_ratelimited_request(
method="POST",
- url="/v3/rules/append",
- payload=rule,
+ url="/v3/rules/replace",
+ payload=rules,
**extra_kwargs,
)
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index b74a99e..00025bb 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -341,7 +341,7 @@ def list(
)
page = PagedResponse[Transaction](
**resp.json(),
- request_id=request_id,
+ request_id=resp.headers.get("x-request-id", request_id),
_resource=self,
_extra_kwargs=extra_kwargs,
)
@@ -362,7 +362,9 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Transaction:
payload=None,
**extra_kwargs,
)
- return Transaction(**resp.json(), request_id=request_id)
+ return Transaction(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def create(
self,
@@ -381,7 +383,9 @@ def create(
payload_json_str=pydantic_json(EnrichmentInput(transactions=transactions)),
**extra_kwargs,
)
- return EnrichmentResult(**resp.json(), request_id=request_id)
+ return EnrichmentResult(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def assign(
self,
@@ -403,4 +407,6 @@ def assign(
},
**extra_kwargs,
)
- return Transaction(**resp.json(), request_id=request_id)
+ return Transaction(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
diff --git a/ntropy_sdk/v2/ntropy_sdk.py b/ntropy_sdk/v2/ntropy_sdk.py
index 347bc9b..9b87d22 100644
--- a/ntropy_sdk/v2/ntropy_sdk.py
+++ b/ntropy_sdk/v2/ntropy_sdk.py
@@ -1048,13 +1048,13 @@ def statement_info(self) -> StatementInfo:
"""
url = f"/datasources/bank_statements/{self.bs_id}/statement-info"
request_id = uuid.uuid4().hex
- json_resp = self.sdk.retry_ratelimited_request(
+ resp = self.sdk.retry_ratelimited_request(
"GET",
url,
None,
request_id=request_id,
- ).json()
- return StatementInfo(**json_resp, request_id=request_id)
+ )
+ return StatementInfo(**resp.json(), request_id=resp.headers.get("x-request-id", request_id))
def poll(self) -> BankStatement:
"""Polls the current bank statement status and returns the server response
diff --git a/ntropy_sdk/webhooks.py b/ntropy_sdk/webhooks.py
index 542bc5e..105ccf9 100644
--- a/ntropy_sdk/webhooks.py
+++ b/ntropy_sdk/webhooks.py
@@ -76,7 +76,7 @@ def list(
)
page = PagedResponse[Webhook](
**resp.json(),
- request_id=request_id,
+ request_id=resp.headers.get("x-request-id", request_id),
_resource=self,
_extra_kwargs=extra_kwargs,
)
@@ -106,7 +106,9 @@ def create(
},
**extra_kwargs,
)
- return Webhook(**resp.json(), request_id=request_id)
+ return Webhook(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Webhook:
request_id = extra_kwargs.get("request_id")
@@ -118,7 +120,9 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Webhook:
url=f"/v3/webhooks/{id}",
**extra_kwargs,
)
- return Webhook(**resp.json(), request_id=request_id)
+ return Webhook(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def delete(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"):
request_id = extra_kwargs.get("request_id")
From bb44a54cb8345fb3511701521b4bd8e2273f19c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 22 Oct 2024 13:02:26 +0100
Subject: [PATCH 26/64] remove group_id
---
ntropy_sdk/transactions.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 00025bb..5537baf 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -252,9 +252,6 @@ class Recurrence(BaseModel):
description="Whether the transaction is a one-time transfer `one-off`, regularly with varying pricing "
"`recurring` or with fixed pricing `subscription`",
)
- group_id: Optional[str] = Field(
- None, description="If the transaction is recurrent, the group it belongs to."
- )
class TransactionErrorCode(str, enum.Enum):
From d49401d90c29ade8fe4f93d4bb1cab233df3fae7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 22 Oct 2024 23:32:38 +0100
Subject: [PATCH 27/64] bank_statements: add error
---
ntropy_sdk/bank_statements.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 10f617b..34e250f 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -8,7 +8,11 @@
from pydantic import BaseModel, Field, NonNegativeFloat
from ntropy_sdk.v2.bank_statements import StatementInfo
-from ntropy_sdk.v2.errors import NtropyDatasourceError
+from ntropy_sdk.v2.errors import (
+ NtropyDatasourceError,
+ NtropyTimeoutError,
+ NtropyBankStatementError,
+)
from ntropy_sdk.utils import EntryType
from ntropy_sdk.paging import PagedResponse
@@ -29,6 +33,11 @@ class BankStatementFile(BaseModel):
size: Optional[int]
+class BankStatementError(BaseModel):
+ code: str
+ message: str
+
+
class BankStatementJob(BaseModel):
id: str
name: Optional[str]
@@ -36,6 +45,7 @@ class BankStatementJob(BaseModel):
created_at: datetime
file: BankStatementFile
request_id: Optional[str] = None
+ error: BankStatementError | None = None
def is_completed(self):
return self.status == BankStatementJobStatus.COMPLETED
From 4597e52cbd1c7ce9c7920850b2f1f4a738f01bca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 22 Oct 2024 23:32:50 +0100
Subject: [PATCH 28/64] bank_statements: fix schema
---
ntropy_sdk/bank_statements.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 34e250f..1c6f718 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -90,11 +90,11 @@ class BankStatementTransaction(BaseModel):
entry_type: EntryType
amount: NonNegativeFloat
running_balance: Optional[float]
- iso_currency_code: str = Field(
+ currency: str = Field(
description="The currency of the transaction in ISO 4217 format"
)
description: str
- transaction_id: str = Field(
+ id: str = Field(
description="A generated unique identifier for the transaction", min_length=1
)
@@ -109,9 +109,10 @@ class BankStatementAccount(BaseModel):
total_incoming: Optional[float]
total_outgoing: Optional[float]
transactions: List[BankStatementTransaction]
+ request_id: Optional[str] = None
-class BankStatementResults(BankStatementJob):
+class BankStatementResults(BaseModel):
accounts: List[BankStatementAccount]
From 6e62863ccc3f1d432ca9e1a2f32d85ada7f5392c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 22 Oct 2024 23:33:11 +0100
Subject: [PATCH 29/64] wait_for_results
---
ntropy_sdk/bank_statements.py | 62 +++++++++++++++++++----------------
ntropy_sdk/batches.py | 28 ++++++++++++++++
ntropy_sdk/v2/errors.py | 18 ++++++++--
3 files changed, 78 insertions(+), 30 deletions(-)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 1c6f718..1bbeec1 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -53,34 +53,6 @@ def is_completed(self):
def is_error(self):
return self.status == BankStatementJobStatus.ERROR
- def wait_for_results(
- self,
- sdk: "SDK",
- *,
- timeout: int = 4 * 60 * 60,
- poll_interval: int = 10,
- **extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> "BankStatementResults":
- """Continuously polls the status of this job, blocking until the job either succeeds
- or fails. If the job is successful, returns the results. Otherwise, raises an
- `NtropyDatasourceError` exception."""
-
- finish_statuses = [
- BankStatementJobStatus.COMPLETED,
- BankStatementJobStatus.ERROR,
- ]
- start_time = time.monotonic()
- while time.monotonic() - start_time < timeout:
- self.status = sdk.bank_statements.get(id=self.id).status
- if self.status in finish_statuses:
- break
- time.sleep(poll_interval)
-
- if self.is_completed():
- return sdk.bank_statements.results(id=self.id, **extra_kwargs)
- else:
- raise NtropyDatasourceError()
-
class Config:
extra = "allow"
@@ -230,3 +202,37 @@ def overview(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> StatementI
return StatementInfo(
**resp.json(), request_id=resp.headers.get("x-request-id", request_id)
)
+
+ def wait_for_results(
+ self,
+ id: str,
+ *,
+ timeout: int = 10 * 60 * 60,
+ poll_interval: int = 10,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> "BankStatementResults":
+ """Continuously polls the status of this job, blocking until the job either succeeds
+ or fails. If the job is successful, returns the results. Otherwise, raises an
+ `NtropyBankStatementError` on a bank statement processing error or `NtropyTimeoutError`
+ if the `timeout` is exceeded."""
+
+ finish_statuses = [
+ BankStatementJobStatus.COMPLETED,
+ BankStatementJobStatus.ERROR,
+ ]
+ start_time = time.monotonic()
+ stmt = None
+ while time.monotonic() - start_time < timeout:
+ stmt = self._sdk.bank_statements.get(id=id)
+ if stmt.status in finish_statuses:
+ break
+ time.sleep(poll_interval)
+
+ if stmt and stmt.status not in finish_statuses:
+ raise NtropyTimeoutError()
+ if stmt.is_error():
+ assert stmt.error is not None
+ raise NtropyBankStatementError(
+ id=stmt.id, code=stmt.error.code, message=stmt.error.message
+ )
+ return self._sdk.bank_statements.results(id=id, **extra_kwargs)
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index ef21f5e..c8e9423 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -185,3 +185,31 @@ def results(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> BatchResult
return BatchResult(
**resp.json(), request_id=resp.headers.get("x-request-id", request_id)
)
+
+ def wait_for_results(
+ self,
+ id: str,
+ *,
+ timeout: int = 10 * 60 * 60,
+ poll_interval: int = 10,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> "BatchResult":
+ """Continuously polls the status of this batch, blocking until the batch
+ either succeeds or fails. Raises `NtropyTimeoutError` if the `timeout` is exceeded or `NtropyBatchError`
+ if the batch contains errors."""
+
+ finish_statuses = [BatchStatus.COMPLETED, BatchStatus.ERROR]
+ start_time = time.monotonic()
+
+ batch = None
+ while time.monotonic() - start_time < timeout:
+ batch = self._sdk.batches.get(id=id)
+ if batch.status in finish_statuses:
+ break
+ time.sleep(poll_interval)
+
+ if batch and batch.status not in finish_statuses:
+ raise NtropyTimeoutError()
+ if batch.is_error():
+ raise NtropyBatchError("Some transactions contain errors", id=batch.id)
+ return self._sdk.batches.results(id=id, **extra_kwargs)
diff --git a/ntropy_sdk/v2/errors.py b/ntropy_sdk/v2/errors.py
index 10d5e91..f3840a6 100644
--- a/ntropy_sdk/v2/errors.py
+++ b/ntropy_sdk/v2/errors.py
@@ -14,9 +14,9 @@ def __str__(self):
class NtropyBatchError(Exception):
"""One or more errors in one or more transactions of a submitted transaction batch"""
- def __init__(self, message, batch_id=None, errors=None):
+ def __init__(self, message, id=None, errors=None):
super().__init__(message)
- self.batch_id = batch_id
+ self.id = id
self.errors = errors
@@ -33,6 +33,20 @@ def __str__(self):
return f"{self.DESCRIPTION}: {self.error_code}: {self.error}"
+class NtropyBankStatementError(Exception):
+ """Errors in processing underlying document"""
+
+ DESCRIPTION = "Error processing submitted document"
+
+ def __init__(self, id: str, code, message):
+ self.id = id
+ self.code = code
+ self.message = message
+
+ def __str__(self):
+ return f"{self.DESCRIPTION}: {self.code}: {self.message}"
+
+
class NtropyTimeoutError(NtropyError):
DESCRIPTION = "Operation timed out"
From 3b21cdc44c56e35e9d294aff8eb3343c287f765d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 22 Oct 2024 23:33:26 +0100
Subject: [PATCH 30/64] fixes
---
ntropy_sdk/bank_statements.py | 2 +-
ntropy_sdk/batches.py | 29 ++++-------------------------
ntropy_sdk/transactions.py | 2 --
3 files changed, 5 insertions(+), 28 deletions(-)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 1bbeec1..169fc79 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -194,7 +194,7 @@ def overview(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> StatementI
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
- method="GET",
+ method="POST",
url=f"/v3/bank_statements/{id}/overview",
payload=None,
**extra_kwargs,
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index c8e9423..f6b0c23 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -16,7 +16,7 @@
)
if TYPE_CHECKING:
- from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import ExtraKwargs, NtropyTimeoutError
from ntropy_sdk import SDK
from typing_extensions import Unpack
@@ -50,30 +50,9 @@ def is_completed(self):
def is_error(self):
return self.status == BatchStatus.ERROR
- def wait_for_results(
- self,
- sdk: "SDK",
- *,
- timeout: int = 4 * 60 * 60,
- poll_interval: int = 10,
- **extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> "BatchResult":
- """Continuously polls the status of this batch, blocking until the batch
- either succeeds or fails. If successful, returns the results. Otherwise,
- raises an `NtropyBatchError` exception."""
-
- finish_statuses = [BatchStatus.COMPLETED, BatchStatus.ERROR]
- start_time = time.monotonic()
- while time.monotonic() - start_time < timeout:
- self.status = sdk.v3.batches.get(id=self.id).status
- if self.status in finish_statuses:
- break
- time.sleep(poll_interval)
- if self.is_completed():
- return sdk.v3.batches.results(id=self.id, **extra_kwargs)
- else:
- raise NtropyBatchError(f"Batch[{self.id}] contains errors")
+class EnrichmentResult(BaseModel):
+ transactions: List[EnrichedTransaction]
class BatchResult(BaseModel):
@@ -87,7 +66,7 @@ class BatchResult(BaseModel):
description="The total number of transactions in the batch result."
)
status: BatchStatus = Field(description="The current status of the batch job.")
- results: List[EnrichedTransaction] = Field(
+ results: EnrichmentResult = Field(
description="A list of enriched transactions resulting from the enrichment of this batch."
)
request_id: Optional[str] = None
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 5537baf..4d6d14c 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -256,8 +256,6 @@ class Recurrence(BaseModel):
class TransactionErrorCode(str, enum.Enum):
ACCOUNT_HOLDER_NOT_FOUND = "account_holder_not_found"
- UNSUPPORTED_CURRENCY = "unsupported_currency"
- UNSUPPORTED_COUNTRY = "unsupported_country"
INTERNAL_ERROR = "internal_error"
From 68ed9ff23be8f57483a2531e0c377e250d650dff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 22 Oct 2024 23:49:37 +0100
Subject: [PATCH 31/64] optional
---
ntropy_sdk/bank_statements.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 169fc79..7c37e54 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -45,7 +45,7 @@ class BankStatementJob(BaseModel):
created_at: datetime
file: BankStatementFile
request_id: Optional[str] = None
- error: BankStatementError | None = None
+ error: Optional[BankStatementError] = None
def is_completed(self):
return self.status == BankStatementJobStatus.COMPLETED
From 8f89745b3f84e852c0fab575d15137f688ed22d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 22 Oct 2024 23:57:53 +0100
Subject: [PATCH 32/64] versions
---
ntropy_sdk/__init__.py | 2 +-
ntropy_sdk/v2/ntropy_sdk.py | 4 +++-
ntropy_sdk/version.py | 4 +++-
3 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index d7b7dbf..56410d9 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -1,6 +1,6 @@
from .version import VERSION
-__version__ = VERSION
+__version__ = "4.26.0"
from typing import TYPE_CHECKING, Optional
import requests
diff --git a/ntropy_sdk/v2/ntropy_sdk.py b/ntropy_sdk/v2/ntropy_sdk.py
index 9b87d22..ab761ad 100644
--- a/ntropy_sdk/v2/ntropy_sdk.py
+++ b/ntropy_sdk/v2/ntropy_sdk.py
@@ -1054,7 +1054,9 @@ def statement_info(self) -> StatementInfo:
None,
request_id=request_id,
)
- return StatementInfo(**resp.json(), request_id=resp.headers.get("x-request-id", request_id))
+ return StatementInfo(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def poll(self) -> BankStatement:
"""Polls the current bank statement status and returns the server response
diff --git a/ntropy_sdk/version.py b/ntropy_sdk/version.py
index 3486778..ba908e1 100644
--- a/ntropy_sdk/version.py
+++ b/ntropy_sdk/version.py
@@ -1 +1,3 @@
-VERSION = "4.26.0"
+from ntropy_sdk import __version__
+
+VERSION = __version__
From 504710fce2e4efdb04a341510eb953e32e2bebdd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 22 Oct 2024 23:57:55 +0100
Subject: [PATCH 33/64] =?UTF-8?q?Bump=20version:=204.26.0=20=E2=86=92=205.?=
=?UTF-8?q?0.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ntropy_sdk/__init__.py | 2 +-
setup.cfg | 2 +-
setup.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 56410d9..1d8e604 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -1,6 +1,6 @@
from .version import VERSION
-__version__ = "4.26.0"
+__version__ = "5.0.0"
from typing import TYPE_CHECKING, Optional
import requests
diff --git a/setup.cfg b/setup.cfg
index ac4a9af..849391a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.26.0
+current_version = 5.0.0
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(rc(?P\d+))?
diff --git a/setup.py b/setup.py
index d50df9d..6628867 100644
--- a/setup.py
+++ b/setup.py
@@ -54,6 +54,6 @@
test_suite="tests",
tests_require=test_requirements,
url="https://github.com/ntropy-network/ntropy-sdk",
- version="4.26.0",
+ version="5.0.0",
zip_safe=False,
)
From 10523fadc3cbf75f547cdf28c3d28874a591fab5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Wed, 23 Oct 2024 00:02:08 +0100
Subject: [PATCH 34/64] =?UTF-8?q?Bump=20version:=205.0.0=20=E2=86=92=205.0?=
=?UTF-8?q?.0rc1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ntropy_sdk/__init__.py | 2 +-
setup.cfg | 2 +-
setup.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 1d8e604..387c64f 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -1,6 +1,6 @@
from .version import VERSION
-__version__ = "5.0.0"
+__version__ = "5.0.0rc1"
from typing import TYPE_CHECKING, Optional
import requests
diff --git a/setup.cfg b/setup.cfg
index 849391a..8043785 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.0.0
+current_version = 5.0.0rc1
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(rc(?P\d+))?
diff --git a/setup.py b/setup.py
index 6628867..36a246f 100644
--- a/setup.py
+++ b/setup.py
@@ -54,6 +54,6 @@
test_suite="tests",
tests_require=test_requirements,
url="https://github.com/ntropy-network/ntropy-sdk",
- version="5.0.0",
+ version="5.0.0rc1",
zip_safe=False,
)
From e5af925c3bdd2c4beeb71d9bb40905a970884198 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Wed, 23 Oct 2024 00:48:11 +0100
Subject: [PATCH 35/64] version
---
ntropy_sdk/__init__.py | 2 --
ntropy_sdk/http.py | 2 +-
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 387c64f..a02745e 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -1,5 +1,3 @@
-from .version import VERSION
-
__version__ = "5.0.0rc1"
from typing import TYPE_CHECKING, Optional
diff --git a/ntropy_sdk/http.py b/ntropy_sdk/http.py
index bc7e7e5..faffdeb 100644
--- a/ntropy_sdk/http.py
+++ b/ntropy_sdk/http.py
@@ -6,7 +6,7 @@
import requests
-from ntropy_sdk import VERSION
+from ntropy_sdk.version import VERSION
from ntropy_sdk.v2.errors import error_from_http_status_code, NtropyError
From 1186dfeb9bf630f477bece6142c125631f0fc040 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Thu, 24 Oct 2024 17:32:01 +0100
Subject: [PATCH 36/64] add transaction ids
---
ntropy_sdk/transactions.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 4d6d14c..f285426 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -235,6 +235,9 @@ class RecurrenceGroup(BaseModel):
categories: Categories = Field(
description="Categories of the transactions in the recurrence group"
)
+ transaction_ids: List[str] = Field(
+ description="Transactions that belong to the group"
+ )
class RecurrenceGroups(BaseModel):
From 53dc758f68dadcc806c9fe4a1940bd7c0a9ca611 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 25 Oct 2024 12:47:25 +0100
Subject: [PATCH 37/64] update webhook
---
ntropy_sdk/webhooks.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ntropy_sdk/webhooks.py b/ntropy_sdk/webhooks.py
index 105ccf9..70f5afb 100644
--- a/ntropy_sdk/webhooks.py
+++ b/ntropy_sdk/webhooks.py
@@ -24,7 +24,7 @@ class _Unset:
"reports.pending",
"bank_statements.processing",
"bank_statements.processed",
- "bank_statements.failed",
+ "bank_statements.error",
"batches.completed",
"batches.error",
]
From cf3f531e9fc19d3b43bd22d7a363207bc38b97dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Fri, 25 Oct 2024 14:50:46 +0100
Subject: [PATCH 38/64] added categories
---
ntropy_sdk/categories.py | 62 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 ntropy_sdk/categories.py
diff --git a/ntropy_sdk/categories.py b/ntropy_sdk/categories.py
new file mode 100644
index 0000000..781b070
--- /dev/null
+++ b/ntropy_sdk/categories.py
@@ -0,0 +1,62 @@
+from typing import TYPE_CHECKING
+import uuid
+
+from ntropy_sdk.account_holders import AccountHolderType
+
+if TYPE_CHECKING:
+ from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import SDK
+ from typing_extensions import Unpack
+
+
+class CategoriesResource:
+ def __init__(self, sdk: "SDK"):
+ self._sdk = sdk
+
+ def get(
+ self,
+ account_holder_type: AccountHolderType,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> dict:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="GET",
+ url=f"/v3/categories/{account_holder_type.value}",
+ **extra_kwargs,
+ )
+ return resp.json()
+
+ def set(
+ self,
+ account_holder_type: AccountHolderType,
+ categories: dict,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ):
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="POST",
+ url=f"/v3/categories/{account_holder_type.value}",
+ json=categories,
+ **extra_kwargs,
+ )
+
+ def reset(
+ self,
+ account_holder_type: AccountHolderType,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ):
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="POST",
+ url=f"/v3/categories/{account_holder_type.value}/reset",
+ **extra_kwargs,
+ )
From 8728185dd5f31a77bb32dcdb53e9e6a30f3e6076 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:46:16 +0000
Subject: [PATCH 39/64] widen mcc validation
---
ntropy_sdk/v2/ntropy_sdk.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ntropy_sdk/v2/ntropy_sdk.py b/ntropy_sdk/v2/ntropy_sdk.py
index ab761ad..9abbcc0 100644
--- a/ntropy_sdk/v2/ntropy_sdk.py
+++ b/ntropy_sdk/v2/ntropy_sdk.py
@@ -148,7 +148,7 @@ class Transaction(BaseModel):
)
mcc: Optional[int] = Field(
None,
- ge=700,
+ ge=0,
le=9999,
description="The Merchant Category Code of the merchant, according to ISO 18245.",
)
From 69ef4a2e646c3864b9736caa313a3431956ac972 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:46:23 +0000
Subject: [PATCH 40/64] remove intermediary type
---
ntropy_sdk/transactions.py | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index f285426..7f52c35 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -148,14 +148,8 @@ class Counterparty(Entity):
type: CounterpartyType
-class IntermediaryType(str, enum.Enum):
- DELIVERY_SERVICE = "delivery_service"
- PAYMENT_PROCESSOR = "payment_processor"
- MARKETPLACE = "marketplace"
-
-
class Intermediary(Entity):
- type: IntermediaryType
+ ...
class Entities(BaseModel):
From 11ab649b63c3228e9e880a1056fdf08ece6d7e98 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 28 Oct 2024 19:27:32 +0000
Subject: [PATCH 41/64] update schemas
---
README.md | 28 +++++++++++-------------
ntropy_sdk/account_holders.py | 12 ++++++++--
ntropy_sdk/batches.py | 31 ++++++++++++--------------
ntropy_sdk/paging.py | 3 +--
ntropy_sdk/transactions.py | 41 ++++++++++++++++++++---------------
requirements_dev.txt | 2 +-
tests/v3/test_sdk.py | 34 +++++++++++------------------
7 files changed, 76 insertions(+), 75 deletions(-)
diff --git a/README.md b/README.md
index a74c8fa..4198163 100644
--- a/README.md
+++ b/README.md
@@ -17,23 +17,21 @@ $ python3 -m pip install --upgrade 'ntropy-sdk'
Enriching your first transaction requires an `SDK` object and an input `Transaction` object. The API key can be set in the environment variable `NTROPY_API_KEY` or in the `SDK` constructor:
```python
-from ntropy_sdk import SDK, TransactionInput, LocationInput
+from ntropy_sdk import SDK
sdk = SDK("YOUR-API-KEY")
-r = sdk.transactions.create([
- TransactionInput(
- id = "4yp49x3tbj9mD8DB4fM8DDY6Yxbx8YP14g565Xketw3tFmn",
- description = "AMAZON WEB SERVICES",
- entry_type = "outgoing",
- amount = 12042.37,
- currency = "USD",
- date = "2021-11-01",
- location = LocationInput(
- country="US"
- ),
- )
-])
-print(r.transactions[0].entities.counterparty)
+r = sdk.transactions.create(
+ id="4yp49x3tbj9mD8DB4fM8DDY6Yxbx8YP14g565Xketw3tFmn",
+ description="AMAZON WEB SERVICES",
+ entry_type="outgoing",
+ amount=12042.37,
+ currency="USD",
+ date="2021-11-01",
+ location=dict(
+ country="US"
+ ),
+)
+print(r)
```
The returned `EnrichedTransaction` contains the added information by Ntropy API. You can consult the Enrichment section of the documentation for more information on the parameters for both `Transaction` and `EnrichedTransaction`.
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index 15d188c..826e1ed 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -93,7 +93,9 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> AccountHolder:
def create(
self,
- account_holder: AccountHolder,
+ id: str,
+ type: AccountHolderType,
+ name: Optional[str] = None,
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> AccountHolder:
"""Create an account holder"""
@@ -105,7 +107,13 @@ def create(
resp = self._sdk.retry_ratelimited_request(
method="POST",
url="/v3/account_holders",
- payload_json_str=pydantic_json(account_holder),
+ payload_json_str=pydantic_json(
+ AccountHolder(
+ id=id,
+ type=type,
+ name=name,
+ )
+ ),
**extra_kwargs,
)
return AccountHolder(
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index f6b0c23..249bf67 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -1,19 +1,16 @@
+import time
+import uuid
from datetime import datetime
from enum import Enum
-import time
from typing import List, Optional, TYPE_CHECKING
-import uuid
from pydantic import BaseModel, Field
-from ntropy_sdk.v2 import NtropyBatchError
-from ntropy_sdk.utils import pydantic_json
from ntropy_sdk.paging import PagedResponse
from ntropy_sdk.transactions import (
EnrichedTransaction,
- EnrichmentInput,
- TransactionInput,
)
+from ntropy_sdk.v2 import NtropyBatchError
if TYPE_CHECKING:
from ntropy_sdk import ExtraKwargs, NtropyTimeoutError
@@ -33,6 +30,7 @@ class Batch(BaseModel):
"""
id: str = Field(description="A unique identifier for the batch.")
+ operation: str = Field(description="Operation for the batch")
status: BatchStatus = Field(description="The current status of the batch.")
created_at: datetime = Field(
description="The timestamp of when the batch was created."
@@ -40,8 +38,8 @@ class Batch(BaseModel):
updated_at: datetime = Field(
description="The timestamp of when the batch was last updated."
)
- progress: int = Field(description="The number of transactions processed so far.")
- total: int = Field(description="The total number of transactions in the batch.")
+ progress: int = Field(description="The number of requests processed so far.")
+ total: int = Field(description="The total number of requests in the batch.")
request_id: Optional[str] = None
def is_completed(self):
@@ -51,14 +49,9 @@ def is_error(self):
return self.status == BatchStatus.ERROR
-class EnrichmentResult(BaseModel):
- transactions: List[EnrichedTransaction]
-
-
class BatchResult(BaseModel):
"""
- The `BatchResult` object represents the result of a batch enrichment job, including its status and
- enriched transactions.
+ The `BatchResult` object represents the result of a batch enrichment job
"""
id: str = Field(description="A unique identifier for the batch.")
@@ -66,7 +59,7 @@ class BatchResult(BaseModel):
description="The total number of transactions in the batch result."
)
status: BatchStatus = Field(description="The current status of the batch job.")
- results: EnrichmentResult = Field(
+ results: list[EnrichedTransaction] = Field(
description="A list of enriched transactions resulting from the enrichment of this batch."
)
request_id: Optional[str] = None
@@ -132,7 +125,8 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Batch:
def create(
self,
- transactions: List[TransactionInput],
+ operation: str,
+ data: List[dict],
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> Batch:
"""Submit a batch of transactions for enrichment"""
@@ -144,7 +138,10 @@ def create(
resp = self._sdk.retry_ratelimited_request(
method="POST",
url="/v3/batches",
- payload_json_str=pydantic_json(EnrichmentInput(transactions=transactions)),
+ payload_json={
+ "operation": operation,
+ "data": data,
+ },
**extra_kwargs,
)
return Batch(
diff --git a/ntropy_sdk/paging.py b/ntropy_sdk/paging.py
index f330bcf..a2e2e58 100644
--- a/ntropy_sdk/paging.py
+++ b/ntropy_sdk/paging.py
@@ -34,8 +34,7 @@ def list(
cursor: str,
limit: Optional[int],
**extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> "PagedResponse[T]":
- ...
+ ) -> "PagedResponse[T]": ...
class PagedResponse(GenericModel, Generic[T]):
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 7f52c35..644dc08 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -148,8 +148,7 @@ class Counterparty(Entity):
type: CounterpartyType
-class Intermediary(Entity):
- ...
+class Intermediary(Entity): ...
class Entities(BaseModel):
@@ -229,9 +228,6 @@ class RecurrenceGroup(BaseModel):
categories: Categories = Field(
description="Categories of the transactions in the recurrence group"
)
- transaction_ids: List[str] = Field(
- description="Transactions that belong to the group"
- )
class RecurrenceGroups(BaseModel):
@@ -276,12 +272,7 @@ class EnrichedTransaction(_EnrichedTransactionBase):
)
-class EnrichmentInput(BaseModel):
- transactions: List[TransactionInput]
-
-
-class EnrichmentResult(BaseModel):
- transactions: List[EnrichedTransaction]
+class EnrichedTransactionResponse(EnrichedTransaction):
request_id: Optional[str] = None
@@ -360,11 +351,16 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Transaction:
def create(
self,
- transactions: List[TransactionInput],
+ id: str,
+ description: str,
+ date: str,
+ amount: int,
+ entry_type: str,
+ currency: str,
+ account_holder_id: Optional[str] = None,
+ location: Optional[dict] = None,
**extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> EnrichmentResult:
- """Synchronously enrich transactions"""
-
+ ):
request_id = extra_kwargs.get("request_id")
if request_id is None:
request_id = uuid.uuid4().hex
@@ -372,10 +368,21 @@ def create(
resp = self._sdk.retry_ratelimited_request(
method="POST",
url="/v3/transactions",
- payload_json_str=pydantic_json(EnrichmentInput(transactions=transactions)),
+ payload_json_str=pydantic_json(
+ TransactionInput(
+ id=id,
+ description=description,
+ date=date,
+ amount=amount,
+ entry_type=entry_type,
+ currency=currency,
+ account_holder_id=account_holder_id,
+ location=location,
+ )
+ ),
**extra_kwargs,
)
- return EnrichmentResult(
+ return EnrichedTransactionResponse(
**resp.json(), request_id=resp.headers.get("x-request-id", request_id)
)
diff --git a/requirements_dev.txt b/requirements_dev.txt
index ac0c00d..b2fc590 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -7,4 +7,4 @@ tox==3.14.0
coverage==4.5.4
Sphinx==1.8.5
twine==1.14.0
-requests_toolbelt==0.9.1
+requests_toolbelt==1.0.0
diff --git a/tests/v3/test_sdk.py b/tests/v3/test_sdk.py
index 141547c..c566fbe 100644
--- a/tests/v3/test_sdk.py
+++ b/tests/v3/test_sdk.py
@@ -29,31 +29,23 @@ def test_readme(api_key):
def test_recurrence_groups(sdk):
try:
sdk.account_holders.create(
- AccountHolder(
- id="Xksd9SWd",
- type="consumer",
- )
+ id="Xksd9SWd",
+ type="consumer",
)
except NtropyValueError:
pass
- txs = []
- txs.extend(
- [
- TransactionInput(
- id=f"netflix-{i}",
- description=f"Recurring Debit Purchase Card 1350 #{i} netflix.com Netflix.com CA",
- amount=17.99,
- currency="USD",
- entry_type="outgoing",
- date=f"2021-0{i}-01",
- account_holder_id="Xksd9SWd",
- )
- for i in range(1, 5)
- ]
- )
-
- sdk.transactions.create(txs)
+ for i in range(1, 5):
+ sdk.transactions.create(
+ id=f"netflix-{i}",
+ description=f"Recurring Debit Purchase Card 1350 #{i} netflix.com Netflix.com CA",
+ amount=17.99,
+ currency="USD",
+ entry_type="outgoing",
+ date=f"2021-0{i}-01",
+ account_holder_id="Xksd9SWd",
+ )
+
recurring_groups = sdk.account_holders.recurring_groups("Xksd9SWd")
assert recurring_groups.groups[0].counterparty.website == "netflix.com"
From b0d19a11905368ffd060d542c9c2850af036c137 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 28 Oct 2024 19:42:00 +0000
Subject: [PATCH 42/64] transactions: remove recurrence group
---
ntropy_sdk/transactions.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 644dc08..5633557 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -245,6 +245,9 @@ class Recurrence(BaseModel):
description="Whether the transaction is a one-time transfer `one-off`, regularly with varying pricing "
"`recurring` or with fixed pricing `subscription`",
)
+ group_id: str = Field(
+ description="ID of recurrence group"
+ )
class TransactionErrorCode(str, enum.Enum):
@@ -283,7 +286,6 @@ class Transaction(_EnrichedTransactionBase, _TransactionBase):
min_length=1,
)
- recurrence: Optional[Recurrence] = None
request_id: Optional[str] = None
From c7dc802b18fcb382c0f1dd3f754511fff9cc2f30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 29 Oct 2024 10:15:11 +0000
Subject: [PATCH 43/64] lint
---
ntropy_sdk/paging.py | 3 ++-
ntropy_sdk/transactions.py | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/ntropy_sdk/paging.py b/ntropy_sdk/paging.py
index a2e2e58..f330bcf 100644
--- a/ntropy_sdk/paging.py
+++ b/ntropy_sdk/paging.py
@@ -34,7 +34,8 @@ def list(
cursor: str,
limit: Optional[int],
**extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> "PagedResponse[T]": ...
+ ) -> "PagedResponse[T]":
+ ...
class PagedResponse(GenericModel, Generic[T]):
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 5633557..abd08a9 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -148,7 +148,8 @@ class Counterparty(Entity):
type: CounterpartyType
-class Intermediary(Entity): ...
+class Intermediary(Entity):
+ ...
class Entities(BaseModel):
From a416a9180ce138d16f1e8601b07aa0c26f6a5a24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 29 Oct 2024 12:14:45 +0000
Subject: [PATCH 44/64] overview -> verify
---
ntropy_sdk/bank_statements.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 7c37e54..3a4b7c3 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -185,7 +185,7 @@ def results(
**resp.json(), request_id=resp.headers.get("x-request-id", request_id)
)
- def overview(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> StatementInfo:
+ def verify(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> StatementInfo:
"""Waits for and returns preliminary statement information from the
first page of the PDF. This may not always be consistent with the
final results."""
@@ -195,7 +195,7 @@ def overview(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> StatementI
extra_kwargs["request_id"] = request_id
resp = self._sdk.retry_ratelimited_request(
method="POST",
- url=f"/v3/bank_statements/{id}/overview",
+ url=f"/v3/bank_statements/{id}/verify",
payload=None,
**extra_kwargs,
)
From a52313bc693006966445b5b20d770dfb29594cd1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 29 Oct 2024 15:44:28 +0000
Subject: [PATCH 45/64] added entities
---
ntropy_sdk/__init__.py | 4 ---
ntropy_sdk/entities.py | 68 ++++++++++++++++++++++++++++++++++++++
ntropy_sdk/sdk.py | 8 +++--
ntropy_sdk/transactions.py | 4 +--
4 files changed, 75 insertions(+), 9 deletions(-)
create mode 100644 ntropy_sdk/entities.py
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index a02745e..1d6e07f 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -35,7 +35,3 @@ class ExtraKwargs(TypedDict, total=False):
NtropyValueError,
NtropyRuntimeError,
)
-
-from .transactions import TransactionInput, LocationInput
-
-from .account_holders import AccountHolder
diff --git a/ntropy_sdk/entities.py b/ntropy_sdk/entities.py
new file mode 100644
index 0000000..3d2f5ce
--- /dev/null
+++ b/ntropy_sdk/entities.py
@@ -0,0 +1,68 @@
+import uuid
+from typing import Optional, TYPE_CHECKING, List
+
+from pydantic import BaseModel
+
+if TYPE_CHECKING:
+ from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import SDK
+ from typing_extensions import Unpack
+
+
+class Entity(BaseModel):
+ id: Optional[str]
+ name: Optional[str]
+ website: Optional[str]
+ logo: Optional[str]
+ mccs: List[int]
+
+
+class EntityResponse(Entity):
+ request_id: Optional[str] = None
+
+
+class EntitiesResource:
+ def __init__(self, sdk: "SDK"):
+ self._sdk = sdk
+
+ def get(
+ self,
+ id: str,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> EntityResponse:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="GET",
+ url=f"/v3/entities/{id}",
+ **extra_kwargs,
+ )
+ return EntityResponse(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
+
+ def resolve(
+ self,
+ name: Optional[str] = None,
+ website: Optional[str] = None,
+ location: Optional[str] = None,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> EntityResponse:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="POST",
+ url=f"/v3/entities/resolve",
+ payload={
+ "name": name,
+ "website": website,
+ "location": location,
+ },
+ )
+ return EntityResponse(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
index f0c157d..f60a294 100644
--- a/ntropy_sdk/sdk.py
+++ b/ntropy_sdk/sdk.py
@@ -3,6 +3,8 @@
from ntropy_sdk.account_holders import AccountHoldersResource
from ntropy_sdk.bank_statements import BankStatementsResource
from ntropy_sdk.batches import BatchesResource
+from ntropy_sdk.categories import CategoriesResource
+from ntropy_sdk.entities import EntitiesResource
from ntropy_sdk.http import HttpClient
from ntropy_sdk.rules import RulesResource
from ntropy_sdk.transactions import TransactionsResource
@@ -23,12 +25,14 @@ def __init__(
self.base_url = ALL_REGIONS[region]
self.api_key = api_key
self.http_client = HttpClient()
- self.transactions = TransactionsResource(self)
+ self.account_holders = AccountHoldersResource(self)
self.batches = BatchesResource(self)
self.bank_statements = BankStatementsResource(self)
- self.account_holders = AccountHoldersResource(self)
+ self.categories = CategoriesResource(self)
+ self.entities = EntitiesResource(self)
self.webhooks = WebhooksResource(self)
self.rules = RulesResource(self)
+ self.transactions = TransactionsResource(self)
def retry_ratelimited_request(
self,
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index abd08a9..ed1b0f7 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -246,9 +246,7 @@ class Recurrence(BaseModel):
description="Whether the transaction is a one-time transfer `one-off`, regularly with varying pricing "
"`recurring` or with fixed pricing `subscription`",
)
- group_id: str = Field(
- description="ID of recurrence group"
- )
+ group_id: str = Field(description="ID of recurrence group")
class TransactionErrorCode(str, enum.Enum):
From a13c6de81e9d6e2ba8a8e9683028c4518699888b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 29 Oct 2024 17:00:01 +0000
Subject: [PATCH 46/64] msg
---
ntropy_sdk/batches.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index 249bf67..a9e6e1b 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -172,7 +172,7 @@ def wait_for_results(
) -> "BatchResult":
"""Continuously polls the status of this batch, blocking until the batch
either succeeds or fails. Raises `NtropyTimeoutError` if the `timeout` is exceeded or `NtropyBatchError`
- if the batch contains errors."""
+ if the batch encountered an error during processing."""
finish_statuses = [BatchStatus.COMPLETED, BatchStatus.ERROR]
start_time = time.monotonic()
@@ -187,5 +187,5 @@ def wait_for_results(
if batch and batch.status not in finish_statuses:
raise NtropyTimeoutError()
if batch.is_error():
- raise NtropyBatchError("Some transactions contain errors", id=batch.id)
+ raise NtropyBatchError("Batch terminated with an error", id=batch.id)
return self._sdk.batches.results(id=id, **extra_kwargs)
From 489fec521672e8f25cec47fd04cdec7ce7345e3b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 29 Oct 2024 17:40:19 +0000
Subject: [PATCH 47/64] added reports resources
---
ntropy_sdk/reports.py | 112 ++++++++++++++++++++++++++++++++++++++++++
ntropy_sdk/sdk.py | 2 +
2 files changed, 114 insertions(+)
create mode 100644 ntropy_sdk/reports.py
diff --git a/ntropy_sdk/reports.py b/ntropy_sdk/reports.py
new file mode 100644
index 0000000..9ca1ff5
--- /dev/null
+++ b/ntropy_sdk/reports.py
@@ -0,0 +1,112 @@
+import uuid
+from datetime import datetime
+from typing import Optional, TYPE_CHECKING
+
+from pydantic import BaseModel
+
+from ntropy_sdk.paging import PagedResponse
+
+if TYPE_CHECKING:
+ from ntropy_sdk import ExtraKwargs
+ from ntropy_sdk import SDK
+ from typing_extensions import Unpack
+
+
+class Report(BaseModel):
+ id: str
+ created_at: datetime
+ status: str
+ rejection_reason: Optional[str]
+
+ transaction_id: str
+ description: str
+ fields: dict[str, str]
+
+
+class ReportResponse(Report):
+ request_id: Optional[str] = None
+
+
+class ReportsResource:
+ def __init__(self, sdk: "SDK"):
+ self._sdk = sdk
+
+ def create(
+ self,
+ *,
+ transaction_id: str,
+ description: str,
+ fields: str,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> ReportResponse:
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="POST",
+ url="/v3/reports",
+ payload={
+ "transaction_id": transaction_id,
+ "description": description,
+ "fields": fields,
+ },
+ **extra_kwargs,
+ )
+ return ReportResponse(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
+
+ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Report:
+ """Retrieve an account holder"""
+
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="GET",
+ url=f"/v3/reports/{id}",
+ **extra_kwargs,
+ )
+ return ReportResponse(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
+
+ def list(
+ self,
+ *,
+ created_before: Optional[datetime] = None,
+ created_after: Optional[datetime] = None,
+ status: str = None,
+ cursor: Optional[str] = None,
+ limit: Optional[int] = None,
+ **extra_kwargs: "Unpack[ExtraKwargs]",
+ ) -> PagedResponse[ReportResponse]:
+ """List all account holders"""
+
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ resp = self._sdk.retry_ratelimited_request(
+ method="GET",
+ url="/v3/reports",
+ params={
+ "created_before": created_before,
+ "created_after": created_after,
+ "status": status,
+ "cursor": cursor,
+ "limit": limit,
+ },
+ **extra_kwargs,
+ )
+ page = PagedResponse[ReportResponse](
+ **resp.json(),
+ request_id=resp.headers.get("x-request-id", request_id),
+ _resource=self,
+ _extra_kwargs=extra_kwargs,
+ )
+ for t in page.data:
+ t.request_id = request_id
+ return page
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
index f60a294..1f8916d 100644
--- a/ntropy_sdk/sdk.py
+++ b/ntropy_sdk/sdk.py
@@ -6,6 +6,7 @@
from ntropy_sdk.categories import CategoriesResource
from ntropy_sdk.entities import EntitiesResource
from ntropy_sdk.http import HttpClient
+from ntropy_sdk.reports import ReportsResource
from ntropy_sdk.rules import RulesResource
from ntropy_sdk.transactions import TransactionsResource
from ntropy_sdk.v2.ntropy_sdk import DEFAULT_REGION, ALL_REGIONS
@@ -31,6 +32,7 @@ def __init__(
self.categories = CategoriesResource(self)
self.entities = EntitiesResource(self)
self.webhooks = WebhooksResource(self)
+ self.reports = ReportsResource(self)
self.rules = RulesResource(self)
self.transactions = TransactionsResource(self)
From 7ce15d164f622eb4e5e67ef77e4bd2d524c63bd6 Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Tue, 29 Oct 2024 12:42:45 -0500
Subject: [PATCH 48/64] Add __all__ to __init__.py
---
ntropy_sdk/__init__.py | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 1d6e07f..4405976 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -35,3 +35,23 @@ class ExtraKwargs(TypedDict, total=False):
NtropyValueError,
NtropyRuntimeError,
)
+
+
+__all__ = (
+ "SDK",
+ "NtropyError",
+ "NtropyBatchError",
+ "NtropyDatasourceError",
+ "NtropyTimeoutError",
+ "NtropyHTTPError",
+ "NtropyValidationError",
+ "NtropyQuotaExceededError",
+ "NtropyNotSupportedError",
+ "NtropyResourceOccupiedError",
+ "NtropyServerConnectionError",
+ "NtropyRateLimitError",
+ "NtropyNotFoundError",
+ "NtropyNotAuthorizedError",
+ "NtropyValueError",
+ "NtropyRuntimeError",
+)
From 7eb280da25b50113c7bff647dd2751d5eb813fec Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Tue, 29 Oct 2024 18:01:16 -0500
Subject: [PATCH 49/64] =?UTF-8?q?Bump=20version:=205.0.0rc1=20=E2=86=92=20?=
=?UTF-8?q?5.0.0rc2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ntropy_sdk/__init__.py | 2 +-
setup.cfg | 2 +-
setup.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 4405976..4aa3add 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "5.0.0rc1"
+__version__ = "5.0.0rc2"
from typing import TYPE_CHECKING, Optional
import requests
diff --git a/setup.cfg b/setup.cfg
index 8043785..3219475 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.0.0rc1
+current_version = 5.0.0rc2
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(rc(?P\d+))?
diff --git a/setup.py b/setup.py
index 36a246f..e07f04f 100644
--- a/setup.py
+++ b/setup.py
@@ -54,6 +54,6 @@
test_suite="tests",
tests_require=test_requirements,
url="https://github.com/ntropy-network/ntropy-sdk",
- version="5.0.0rc1",
+ version="5.0.0rc2",
zip_safe=False,
)
From bab653edc46b279d8916864bf21706722c1a1f69 Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Tue, 29 Oct 2024 18:12:13 -0500
Subject: [PATCH 50/64] Update requirements_dev.txt
---
requirements_dev.txt | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/requirements_dev.txt b/requirements_dev.txt
index b2fc590..b0c9448 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,10 +1,9 @@
-pip==19.2.3
bump2version==0.5.11
-wheel==0.33.6
+wheel==0.44.6
watchdog==0.9.0
flake8==3.7.8
tox==3.14.0
coverage==4.5.4
Sphinx==1.8.5
-twine==1.14.0
+twine==5.1.1
requests_toolbelt==1.0.0
From 29ff82b76ea7e05fec6b1f706c266d096d3fa629 Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Thu, 31 Oct 2024 05:39:55 +0000
Subject: [PATCH 51/64] Fix some types and add delete methods
---
ntropy_sdk/bank_statements.py | 12 ++++++++++++
ntropy_sdk/reports.py | 19 ++++++++++++++++---
ntropy_sdk/transactions.py | 16 +++++++++++++++-
ntropy_sdk/webhooks.py | 3 +--
4 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 3a4b7c3..40ecee8 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -236,3 +236,15 @@ def wait_for_results(
id=stmt.id, code=stmt.error.code, message=stmt.error.message
)
return self._sdk.bank_statements.results(id=id, **extra_kwargs)
+
+ def delete(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"):
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="DELETE",
+ url=f"/v3/bank_statements/{id}",
+ payload=None,
+ **extra_kwargs,
+ )
diff --git a/ntropy_sdk/reports.py b/ntropy_sdk/reports.py
index 9ca1ff5..dbc01ef 100644
--- a/ntropy_sdk/reports.py
+++ b/ntropy_sdk/reports.py
@@ -36,7 +36,7 @@ def create(
*,
transaction_id: str,
description: str,
- fields: str,
+ fields: list[str],
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> ReportResponse:
request_id = extra_kwargs.get("request_id")
@@ -58,7 +58,7 @@ def create(
)
def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Report:
- """Retrieve an account holder"""
+ """Retrieve a report"""
request_id = extra_kwargs.get("request_id")
if request_id is None:
@@ -83,7 +83,7 @@ def list(
limit: Optional[int] = None,
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> PagedResponse[ReportResponse]:
- """List all account holders"""
+ """List all reports"""
request_id = extra_kwargs.get("request_id")
if request_id is None:
@@ -110,3 +110,16 @@ def list(
for t in page.data:
t.request_id = request_id
return page
+
+ def delete(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Report:
+ """Delete a report"""
+
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="DELETE",
+ url=f"/v3/reports/{id}",
+ **extra_kwargs,
+ )
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index ed1b0f7..22d7d2b 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -355,7 +355,7 @@ def create(
id: str,
description: str,
date: str,
- amount: int,
+ amount: float,
entry_type: str,
currency: str,
account_holder_id: Optional[str] = None,
@@ -410,3 +410,17 @@ def assign(
return Transaction(
**resp.json(), request_id=resp.headers.get("x-request-id", request_id)
)
+
+ def delete(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"):
+ """Delete a transaction"""
+
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="DELETE",
+ url=f"/v3/transactions/{id}",
+ payload=None,
+ **extra_kwargs,
+ )
diff --git a/ntropy_sdk/webhooks.py b/ntropy_sdk/webhooks.py
index 70f5afb..b5ce1fc 100644
--- a/ntropy_sdk/webhooks.py
+++ b/ntropy_sdk/webhooks.py
@@ -22,8 +22,7 @@ class _Unset:
"reports.resolved",
"reports.rejected",
"reports.pending",
- "bank_statements.processing",
- "bank_statements.processed",
+ "bank_statements.completed",
"bank_statements.error",
"batches.completed",
"batches.error",
From 61f47254251300df5fbe241284f306a3eaad1f4f Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Thu, 31 Oct 2024 06:56:00 +0000
Subject: [PATCH 52/64] Fix batch post
---
ntropy_sdk/batches.py | 2 +-
ntropy_sdk/reports.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index a9e6e1b..70ec98c 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -138,7 +138,7 @@ def create(
resp = self._sdk.retry_ratelimited_request(
method="POST",
url="/v3/batches",
- payload_json={
+ payload={
"operation": operation,
"data": data,
},
diff --git a/ntropy_sdk/reports.py b/ntropy_sdk/reports.py
index dbc01ef..1bc0ddf 100644
--- a/ntropy_sdk/reports.py
+++ b/ntropy_sdk/reports.py
@@ -111,7 +111,7 @@ def list(
t.request_id = request_id
return page
- def delete(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> Report:
+ def delete(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"):
"""Delete a report"""
request_id = extra_kwargs.get("request_id")
From 55d2d1767cf92f259d160d5168573c4047a6dd2c Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Fri, 1 Nov 2024 16:32:53 +0000
Subject: [PATCH 53/64] Fixes
---
ntropy_sdk/account_holders.py | 13 +++++++++++++
ntropy_sdk/reports.py | 2 +-
ntropy_sdk/transactions.py | 2 +-
3 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index 826e1ed..a623ac4 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -138,3 +138,16 @@ def recurring_groups(
groups=[RecurrenceGroup(**r) for r in resp.json()],
request_id=resp.headers.get("x-request-id", request_id),
)
+
+ def delete(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"):
+ """Retrieve an account holder"""
+
+ request_id = extra_kwargs.get("request_id")
+ if request_id is None:
+ request_id = uuid.uuid4().hex
+ extra_kwargs["request_id"] = request_id
+ self._sdk.retry_ratelimited_request(
+ method="DELETE",
+ url=f"/v3/account_holders/{id}",
+ **extra_kwargs,
+ )
diff --git a/ntropy_sdk/reports.py b/ntropy_sdk/reports.py
index 1bc0ddf..cd5cd15 100644
--- a/ntropy_sdk/reports.py
+++ b/ntropy_sdk/reports.py
@@ -20,7 +20,7 @@ class Report(BaseModel):
transaction_id: str
description: str
- fields: dict[str, str]
+ fields: list[str]
class ReportResponse(Report):
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 22d7d2b..6a0f4a2 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -402,7 +402,7 @@ def assign(
resp = self._sdk.retry_ratelimited_request(
method="POST",
url=f"/v3/transactions/{transaction_id}/assign",
- params={
+ payload={
"account_holder_id": account_holder_id,
},
**extra_kwargs,
From 12fb1dbafb6b07c7763d92b473bb453a5af8fac7 Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Fri, 1 Nov 2024 19:12:21 +0000
Subject: [PATCH 54/64] Allow str for account holder type
---
ntropy_sdk/account_holders.py | 4 ++--
ntropy_sdk/categories.py | 16 +++++++++++-----
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index a623ac4..99339d5 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -1,7 +1,7 @@
import uuid
from datetime import datetime
from enum import Enum
-from typing import Optional, TYPE_CHECKING
+from typing import Optional, TYPE_CHECKING, Union
from pydantic import BaseModel, Field
@@ -94,7 +94,7 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> AccountHolder:
def create(
self,
id: str,
- type: AccountHolderType,
+ type: Union[AccountHolderType, str],
name: Optional[str] = None,
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> AccountHolder:
diff --git a/ntropy_sdk/categories.py b/ntropy_sdk/categories.py
index 781b070..71a5b64 100644
--- a/ntropy_sdk/categories.py
+++ b/ntropy_sdk/categories.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Union
import uuid
from ntropy_sdk.account_holders import AccountHolderType
@@ -15,13 +15,15 @@ def __init__(self, sdk: "SDK"):
def get(
self,
- account_holder_type: AccountHolderType,
+ account_holder_type: Union[AccountHolderType, str],
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> dict:
request_id = extra_kwargs.get("request_id")
if request_id is None:
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
+ if not isinstance(account_holder_type, AccountHolderType):
+ account_holder_type = AccountHolderType(account_holder_type)
resp = self._sdk.retry_ratelimited_request(
method="GET",
url=f"/v3/categories/{account_holder_type.value}",
@@ -31,7 +33,7 @@ def get(
def set(
self,
- account_holder_type: AccountHolderType,
+ account_holder_type: Union[AccountHolderType, str],
categories: dict,
**extra_kwargs: "Unpack[ExtraKwargs]",
):
@@ -39,22 +41,26 @@ def set(
if request_id is None:
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
+ if not isinstance(account_holder_type, AccountHolderType):
+ account_holder_type = AccountHolderType(account_holder_type)
self._sdk.retry_ratelimited_request(
method="POST",
url=f"/v3/categories/{account_holder_type.value}",
- json=categories,
+ payload=categories,
**extra_kwargs,
)
def reset(
self,
- account_holder_type: AccountHolderType,
+ account_holder_type: Union[AccountHolderType, str],
**extra_kwargs: "Unpack[ExtraKwargs]",
):
request_id = extra_kwargs.get("request_id")
if request_id is None:
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
+ if not isinstance(account_holder_type, AccountHolderType):
+ account_holder_type = AccountHolderType(account_holder_type)
self._sdk.retry_ratelimited_request(
method="POST",
url=f"/v3/categories/{account_holder_type.value}/reset",
From 17aff0aeea4660b5a0f2216ad2f0f666ba1ca174 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 4 Nov 2024 16:37:32 +0000
Subject: [PATCH 55/64] ah, txs: add created_at
---
ntropy_sdk/account_holders.py | 4 ++++
ntropy_sdk/transactions.py | 5 +++++
2 files changed, 9 insertions(+)
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index 99339d5..fcf0a92 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -32,6 +32,10 @@ class AccountHolder(BaseModel):
default=None,
description="The name of the account holder",
)
+ created_at: datetime = Field(
+ ...,
+ description="Date of creation of the account holder",
+ )
request_id: Optional[str] = None
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 6a0f4a2..605f3a6 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -265,6 +265,11 @@ class _EnrichedTransactionBase(BaseModel):
location: Optional[Location] = None
error: Optional[TransactionError] = None
+ created_at: datetime = Field(
+ ...,
+ description="Date of creation of the transaction",
+ )
+
class EnrichedTransaction(_EnrichedTransactionBase):
id: str = Field(
From 01cd493ac6529bf2b67d9428dfc3312b220d2ab6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 4 Nov 2024 16:37:34 +0000
Subject: [PATCH 56/64] test 404
---
tests/v3/test_sdk.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/tests/v3/test_sdk.py b/tests/v3/test_sdk.py
index c566fbe..4dd2d5b 100644
--- a/tests/v3/test_sdk.py
+++ b/tests/v3/test_sdk.py
@@ -1,11 +1,12 @@
import os
from itertools import islice
+import pytest
+
from ntropy_sdk import (
SDK,
- TransactionInput,
- AccountHolder,
NtropyValueError,
+ NtropyNotFoundError,
)
@@ -26,6 +27,11 @@ def test_readme(api_key):
exec(readme_data, globals())
+def test_404_ah(sdk):
+ with pytest.raises(NtropyNotFoundError):
+ sdk.account_holders.get("non-existent-id")
+
+
def test_recurrence_groups(sdk):
try:
sdk.account_holders.create(
From a26af5aa934cb691188a00cba6ef834b527a2c08 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 4 Nov 2024 16:50:58 +0000
Subject: [PATCH 57/64] typing
---
ntropy_sdk/batches.py | 2 +-
ntropy_sdk/reports.py | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/ntropy_sdk/batches.py b/ntropy_sdk/batches.py
index 70ec98c..60ed868 100644
--- a/ntropy_sdk/batches.py
+++ b/ntropy_sdk/batches.py
@@ -59,7 +59,7 @@ class BatchResult(BaseModel):
description="The total number of transactions in the batch result."
)
status: BatchStatus = Field(description="The current status of the batch job.")
- results: list[EnrichedTransaction] = Field(
+ results: List[EnrichedTransaction] = Field(
description="A list of enriched transactions resulting from the enrichment of this batch."
)
request_id: Optional[str] = None
diff --git a/ntropy_sdk/reports.py b/ntropy_sdk/reports.py
index cd5cd15..79dc5ea 100644
--- a/ntropy_sdk/reports.py
+++ b/ntropy_sdk/reports.py
@@ -1,6 +1,6 @@
import uuid
from datetime import datetime
-from typing import Optional, TYPE_CHECKING
+from typing import Optional, TYPE_CHECKING, List
from pydantic import BaseModel
@@ -20,7 +20,7 @@ class Report(BaseModel):
transaction_id: str
description: str
- fields: list[str]
+ fields: List[str]
class ReportResponse(Report):
@@ -36,7 +36,7 @@ def create(
*,
transaction_id: str,
description: str,
- fields: list[str],
+ fields: List[str],
**extra_kwargs: "Unpack[ExtraKwargs]",
) -> ReportResponse:
request_id = extra_kwargs.get("request_id")
From 0419f55648dea9a7080b57325cc08fa3e5eceba0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 4 Nov 2024 19:23:30 +0000
Subject: [PATCH 58/64] fixes
---
ntropy_sdk/account_holders.py | 23 ++++++++++++++---------
1 file changed, 14 insertions(+), 9 deletions(-)
diff --git a/ntropy_sdk/account_holders.py b/ntropy_sdk/account_holders.py
index fcf0a92..e145fca 100644
--- a/ntropy_sdk/account_holders.py
+++ b/ntropy_sdk/account_holders.py
@@ -20,7 +20,7 @@ class AccountHolderType(str, Enum):
business = "business"
-class AccountHolder(BaseModel):
+class AccountHolderCreate(BaseModel):
id: str = Field(
description="The unique ID of the account holder of the transaction",
min_length=1,
@@ -32,11 +32,14 @@ class AccountHolder(BaseModel):
default=None,
description="The name of the account holder",
)
+ request_id: Optional[str] = None
+
+
+class AccountHolderResponse(AccountHolderCreate):
created_at: datetime = Field(
...,
description="Date of creation of the account holder",
)
- request_id: Optional[str] = None
class AccountHoldersResource:
@@ -51,7 +54,7 @@ def list(
cursor: Optional[str] = None,
limit: Optional[int] = None,
**extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> PagedResponse[AccountHolder]:
+ ) -> PagedResponse[AccountHolderResponse]:
"""List all account holders"""
request_id = extra_kwargs.get("request_id")
@@ -69,7 +72,7 @@ def list(
},
**extra_kwargs,
)
- page = PagedResponse[AccountHolder](
+ page = PagedResponse[AccountHolderResponse](
**resp.json(),
request_id=resp.headers.get("x-request-id", request_id),
_resource=self,
@@ -79,7 +82,9 @@ def list(
t.request_id = request_id
return page
- def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> AccountHolder:
+ def get(
+ self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]"
+ ) -> AccountHolderResponse:
"""Retrieve an account holder"""
request_id = extra_kwargs.get("request_id")
@@ -91,7 +96,7 @@ def get(self, id: str, **extra_kwargs: "Unpack[ExtraKwargs]") -> AccountHolder:
url=f"/v3/account_holders/{id}",
**extra_kwargs,
)
- return AccountHolder(
+ return AccountHolderResponse(
**resp.json(), request_id=resp.headers.get("x-request-id", request_id)
)
@@ -101,7 +106,7 @@ def create(
type: Union[AccountHolderType, str],
name: Optional[str] = None,
**extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> AccountHolder:
+ ) -> AccountHolderResponse:
"""Create an account holder"""
request_id = extra_kwargs.get("request_id")
@@ -112,7 +117,7 @@ def create(
method="POST",
url="/v3/account_holders",
payload_json_str=pydantic_json(
- AccountHolder(
+ AccountHolderCreate(
id=id,
type=type,
name=name,
@@ -120,7 +125,7 @@ def create(
),
**extra_kwargs,
)
- return AccountHolder(
+ return AccountHolderResponse(
**resp.json(), request_id=resp.headers.get("x-request-id", request_id)
)
From 657bf5e8d3d69468250972c9713cdcecc1768cc4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Mon, 4 Nov 2024 19:50:55 +0000
Subject: [PATCH 59/64] categories: return results
---
ntropy_sdk/categories.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/ntropy_sdk/categories.py b/ntropy_sdk/categories.py
index 71a5b64..b0ce7a2 100644
--- a/ntropy_sdk/categories.py
+++ b/ntropy_sdk/categories.py
@@ -43,12 +43,13 @@ def set(
extra_kwargs["request_id"] = request_id
if not isinstance(account_holder_type, AccountHolderType):
account_holder_type = AccountHolderType(account_holder_type)
- self._sdk.retry_ratelimited_request(
+ resp = self._sdk.retry_ratelimited_request(
method="POST",
url=f"/v3/categories/{account_holder_type.value}",
payload=categories,
**extra_kwargs,
)
+ return resp.json()
def reset(
self,
@@ -61,8 +62,9 @@ def reset(
extra_kwargs["request_id"] = request_id
if not isinstance(account_holder_type, AccountHolderType):
account_holder_type = AccountHolderType(account_holder_type)
- self._sdk.retry_ratelimited_request(
+ resp = self._sdk.retry_ratelimited_request(
method="POST",
url=f"/v3/categories/{account_holder_type.value}/reset",
**extra_kwargs,
)
+ return resp.json()
From 7180fa8d4bd648f94f839719d0da593d4e0a0a87 Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Mon, 4 Nov 2024 23:29:30 -0600
Subject: [PATCH 60/64] Update rules resource
---
ntropy_sdk/rules.py | 38 ++++++++++++++++++++++++++++----------
1 file changed, 28 insertions(+), 10 deletions(-)
diff --git a/ntropy_sdk/rules.py b/ntropy_sdk/rules.py
index a5940ae..07d4044 100644
--- a/ntropy_sdk/rules.py
+++ b/ntropy_sdk/rules.py
@@ -1,6 +1,8 @@
-from typing import TYPE_CHECKING, Any, Dict, List
+from typing import TYPE_CHECKING, Any, Dict, List, Optional
import uuid
+from pydantic import BaseModel, Field
+
if TYPE_CHECKING:
from ntropy_sdk import ExtraKwargs
@@ -12,6 +14,16 @@
Rules = List[Dict[str, Any]]
+class TopLevelRule(BaseModel):
+ id: str = Field(
+ description="A generated unique identifier for the top level rule",
+ )
+ request_id: Optional[str] = None
+
+ class Config:
+ extra = "allow"
+
+
class RulesResource:
def __init__(self, sdk: "SDK"):
self._sdk = sdk
@@ -20,22 +32,25 @@ def create(
self,
rule: Rule,
**extra_kwargs: "Unpack[ExtraKwargs]",
- ):
+ ) -> TopLevelRule:
request_id = extra_kwargs.get("request_id")
if request_id is None:
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
- self._sdk.retry_ratelimited_request(
+ resp = self._sdk.retry_ratelimited_request(
method="POST",
url="/v3/rules",
payload=rule,
**extra_kwargs,
)
+ return TopLevelRule(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def get(
self,
**extra_kwargs: "Unpack[ExtraKwargs]",
- ) -> Rules:
+ ) -> List[TopLevelRule]:
request_id = extra_kwargs.get("request_id")
if request_id is None:
request_id = uuid.uuid4().hex
@@ -45,7 +60,7 @@ def get(
url="/v3/rules",
**extra_kwargs,
)
- return resp.json()
+ return [TopLevelRule(**r) for r in resp.json()]
def replace(
self,
@@ -65,7 +80,7 @@ def replace(
def patch(
self,
- index: int,
+ id: str,
rule: Rule,
**extra_kwargs: "Unpack[ExtraKwargs]",
):
@@ -73,16 +88,19 @@ def patch(
if request_id is None:
request_id = uuid.uuid4().hex
extra_kwargs["request_id"] = request_id
- self._sdk.retry_ratelimited_request(
+ resp = self._sdk.retry_ratelimited_request(
method="PATCH",
- url=f"/v3/rules/{index}",
+ url=f"/v3/rules/{id}",
payload=rule,
**extra_kwargs,
)
+ return TopLevelRule(
+ **resp.json(), request_id=resp.headers.get("x-request-id", request_id)
+ )
def delete(
self,
- index: int,
+ id: str,
**extra_kwargs: "Unpack[ExtraKwargs]",
):
request_id = extra_kwargs.get("request_id")
@@ -91,6 +109,6 @@ def delete(
extra_kwargs["request_id"] = request_id
self._sdk.retry_ratelimited_request(
method="DELETE",
- url=f"/v3/rules/{index}",
+ url=f"/v3/rules/{id}",
**extra_kwargs,
)
From 333b3776d369702402edb91298f2d2924309d84c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 5 Nov 2024 11:03:47 +0000
Subject: [PATCH 61/64] sdk: accept session
---
ntropy_sdk/sdk.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/ntropy_sdk/sdk.py b/ntropy_sdk/sdk.py
index 1f8916d..14dad16 100644
--- a/ntropy_sdk/sdk.py
+++ b/ntropy_sdk/sdk.py
@@ -1,5 +1,7 @@
from typing import TYPE_CHECKING, Dict, Optional
+import requests
+
from ntropy_sdk.account_holders import AccountHoldersResource
from ntropy_sdk.bank_statements import BankStatementsResource
from ntropy_sdk.batches import BatchesResource
@@ -22,10 +24,11 @@ def __init__(
self,
api_key: Optional[str] = None,
region: str = DEFAULT_REGION,
+ session: Optional[requests.Session] = None,
):
self.base_url = ALL_REGIONS[region]
self.api_key = api_key
- self.http_client = HttpClient()
+ self.http_client = HttpClient(session=session)
self.account_holders = AccountHoldersResource(self)
self.batches = BatchesResource(self)
self.bank_statements = BankStatementsResource(self)
From 2433e8bda3b9475dec07a28748e8c32d22158d38 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Tue, 5 Nov 2024 11:15:14 +0000
Subject: [PATCH 62/64] sdk: filter out null params
---
ntropy_sdk/http.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/ntropy_sdk/http.py b/ntropy_sdk/http.py
index faffdeb..50b37dc 100644
--- a/ntropy_sdk/http.py
+++ b/ntropy_sdk/http.py
@@ -86,6 +86,9 @@ def retry_ratelimited_request(
headers.update(extra_headers)
backoff = 1
+ if params is not None:
+ params = {k: v for k, v in params.items() if v is not None}
+
for _ in range(retries):
try:
resp = cur_session.request(
From 0f6821b0eb2e615a5bd8e02b8dc917f4be1b3bf3 Mon Sep 17 00:00:00 2001
From: Matthew Tran <0e4ef622@gmail.com>
Date: Tue, 5 Nov 2024 08:18:45 -0600
Subject: [PATCH 63/64] Add BankStatementTransaction.to_transaction_input
---
ntropy_sdk/bank_statements.py | 24 +++++++++++++++++++++---
ntropy_sdk/transactions.py | 2 +-
2 files changed, 22 insertions(+), 4 deletions(-)
diff --git a/ntropy_sdk/bank_statements.py b/ntropy_sdk/bank_statements.py
index 40ecee8..0681e74 100644
--- a/ntropy_sdk/bank_statements.py
+++ b/ntropy_sdk/bank_statements.py
@@ -7,14 +7,15 @@
from pydantic import BaseModel, Field, NonNegativeFloat
+from ntropy_sdk.paging import PagedResponse
+from ntropy_sdk.transactions import LocationInput, TransactionInput
+from ntropy_sdk.utils import EntryType
from ntropy_sdk.v2.bank_statements import StatementInfo
from ntropy_sdk.v2.errors import (
+ NtropyBankStatementError,
NtropyDatasourceError,
NtropyTimeoutError,
- NtropyBankStatementError,
)
-from ntropy_sdk.utils import EntryType
-from ntropy_sdk.paging import PagedResponse
if TYPE_CHECKING:
from ntropy_sdk import ExtraKwargs
@@ -70,6 +71,23 @@ class BankStatementTransaction(BaseModel):
description="A generated unique identifier for the transaction", min_length=1
)
+ def to_transaction_input(
+ self,
+ *,
+ account_holder_id: Optional[str] = None,
+ location: Optional[LocationInput] = None,
+ ) -> TransactionInput:
+ return TransactionInput(
+ id=self.id,
+ description=self.description,
+ date=self.date,
+ amount=self.amount,
+ entry_type=self.entry_type,
+ currency=self.currency,
+ account_holder_id=account_holder_id,
+ location=location,
+ )
+
class BankStatementAccount(BaseModel):
number: Optional[str]
diff --git a/ntropy_sdk/transactions.py b/ntropy_sdk/transactions.py
index 605f3a6..69c79d6 100644
--- a/ntropy_sdk/transactions.py
+++ b/ntropy_sdk/transactions.py
@@ -64,7 +64,7 @@ class _TransactionBase(BaseModel):
class TransactionInput(_TransactionBase):
account_holder_id: Optional[str] = Field(
- None,
+ default=None,
description="The id of the account holder. Unsetting it will disable categorization.",
)
From fc51d7e9574172ac432e81550a9c09556ee7049f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Santos?=
<17305792+adgsantos@users.noreply.github.com>
Date: Wed, 6 Nov 2024 14:50:14 +0000
Subject: [PATCH 64/64] =?UTF-8?q?Bump=20version:=205.0.0rc2=20=E2=86=92=20?=
=?UTF-8?q?5.0.1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ntropy_sdk/__init__.py | 2 +-
setup.cfg | 3 +--
setup.py | 2 +-
3 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/ntropy_sdk/__init__.py b/ntropy_sdk/__init__.py
index 4aa3add..1c82db6 100644
--- a/ntropy_sdk/__init__.py
+++ b/ntropy_sdk/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "5.0.0rc2"
+__version__ = "5.0.1"
from typing import TYPE_CHECKING, Optional
import requests
diff --git a/setup.cfg b/setup.cfg
index 3219475..9f73df7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 5.0.0rc2
+current_version = 5.0.1
commit = True
tag = True
parse = (?P\d+)\.(?P\d+)\.(?P\d+)(rc(?P\d+))?
@@ -22,4 +22,3 @@ universal = 1
exclude = docs
[aliases]
-
diff --git a/setup.py b/setup.py
index e07f04f..b763862 100644
--- a/setup.py
+++ b/setup.py
@@ -54,6 +54,6 @@
test_suite="tests",
tests_require=test_requirements,
url="https://github.com/ntropy-network/ntropy-sdk",
- version="5.0.0rc2",
+ version="5.0.1",
zip_safe=False,
)