ynab-cli 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ynab_cli/__init__.py +0 -0
- ynab_cli/adapters/__init__.py +0 -0
- ynab_cli/adapters/rich/__init__.py +0 -0
- ynab_cli/adapters/rich/io.py +38 -0
- ynab_cli/adapters/textual/__init__.py +0 -0
- ynab_cli/adapters/textual/io.py +62 -0
- ynab_cli/adapters/ynab/__init__.py +8 -0
- ynab_cli/adapters/ynab/api/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/accounts/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/accounts/create_account.py +186 -0
- ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +177 -0
- ynab_cli/adapters/ynab/api/accounts/get_accounts.py +184 -0
- ynab_cli/adapters/ynab/api/budgets/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +188 -0
- ynab_cli/adapters/ynab/api/budgets/get_budget_settings_by_id.py +163 -0
- ynab_cli/adapters/ynab/api/budgets/get_budgets.py +171 -0
- ynab_cli/adapters/ynab/api/categories/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/categories/get_categories.py +188 -0
- ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +180 -0
- ynab_cli/adapters/ynab/api/categories/get_month_category_by_id.py +194 -0
- ynab_cli/adapters/ynab/api/categories/update_category.py +199 -0
- ynab_cli/adapters/ynab/api/categories/update_month_category.py +213 -0
- ynab_cli/adapters/ynab/api/months/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/months/get_budget_month.py +177 -0
- ynab_cli/adapters/ynab/api/months/get_budget_months.py +184 -0
- ynab_cli/adapters/ynab/api/payee_locations/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/payee_locations/get_payee_location_by_id.py +176 -0
- ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +163 -0
- ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations_by_payee.py +176 -0
- ynab_cli/adapters/ynab/api/payees/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +176 -0
- ynab_cli/adapters/ynab/api/payees/get_payees.py +184 -0
- ynab_cli/adapters/ynab/api/payees/update_payee.py +199 -0
- ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +186 -0
- ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +176 -0
- ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transaction_by_id.py +176 -0
- ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +184 -0
- ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +199 -0
- ynab_cli/adapters/ynab/api/transactions/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/transactions/create_transaction.py +202 -0
- ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +176 -0
- ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +176 -0
- ynab_cli/adapters/ynab/api/transactions/get_transactions.py +227 -0
- ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +236 -0
- ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +236 -0
- ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +236 -0
- ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +236 -0
- ynab_cli/adapters/ynab/api/transactions/import_transactions.py +183 -0
- ynab_cli/adapters/ynab/api/transactions/update_transaction.py +199 -0
- ynab_cli/adapters/ynab/api/transactions/update_transactions.py +177 -0
- ynab_cli/adapters/ynab/api/user/__init__.py +1 -0
- ynab_cli/adapters/ynab/api/user/get_user.py +130 -0
- ynab_cli/adapters/ynab/client.py +268 -0
- ynab_cli/adapters/ynab/errors.py +16 -0
- ynab_cli/adapters/ynab/models/__init__.py +229 -0
- ynab_cli/adapters/ynab/models/account.py +345 -0
- ynab_cli/adapters/ynab/models/account_response.py +65 -0
- ynab_cli/adapters/ynab/models/account_response_data.py +65 -0
- ynab_cli/adapters/ynab/models/account_type.py +23 -0
- ynab_cli/adapters/ynab/models/accounts_response.py +65 -0
- ynab_cli/adapters/ynab/models/accounts_response_data.py +81 -0
- ynab_cli/adapters/ynab/models/budget_detail.py +405 -0
- ynab_cli/adapters/ynab/models/budget_detail_response.py +65 -0
- ynab_cli/adapters/ynab/models/budget_detail_response_data.py +73 -0
- ynab_cli/adapters/ynab/models/budget_settings.py +115 -0
- ynab_cli/adapters/ynab/models/budget_settings_response.py +65 -0
- ynab_cli/adapters/ynab/models/budget_settings_response_data.py +65 -0
- ynab_cli/adapters/ynab/models/budget_summary.py +216 -0
- ynab_cli/adapters/ynab/models/budget_summary_response.py +65 -0
- ynab_cli/adapters/ynab/models/budget_summary_response_data.py +91 -0
- ynab_cli/adapters/ynab/models/bulk_response.py +65 -0
- ynab_cli/adapters/ynab/models/bulk_response_data.py +65 -0
- ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +68 -0
- ynab_cli/adapters/ynab/models/bulk_transactions.py +75 -0
- ynab_cli/adapters/ynab/models/categories_response.py +65 -0
- ynab_cli/adapters/ynab/models/categories_response_data.py +81 -0
- ynab_cli/adapters/ynab/models/category.py +519 -0
- ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +15 -0
- ynab_cli/adapters/ynab/models/category_goal_type_type_2_type_1.py +15 -0
- ynab_cli/adapters/ynab/models/category_goal_type_type_3_type_1.py +15 -0
- ynab_cli/adapters/ynab/models/category_group.py +85 -0
- ynab_cli/adapters/ynab/models/category_group_with_categories.py +108 -0
- ynab_cli/adapters/ynab/models/category_response.py +65 -0
- ynab_cli/adapters/ynab/models/category_response_data.py +65 -0
- ynab_cli/adapters/ynab/models/currency_format_type_0.py +117 -0
- ynab_cli/adapters/ynab/models/date_format_type_0.py +61 -0
- ynab_cli/adapters/ynab/models/error_detail.py +75 -0
- ynab_cli/adapters/ynab/models/error_response.py +65 -0
- ynab_cli/adapters/ynab/models/existing_transaction.py +322 -0
- ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +12 -0
- ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +12 -0
- ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +12 -0
- ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +12 -0
- ynab_cli/adapters/ynab/models/get_transactions_type.py +12 -0
- ynab_cli/adapters/ynab/models/hybrid_transaction.py +567 -0
- ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +12 -0
- ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +65 -0
- ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +84 -0
- ynab_cli/adapters/ynab/models/loan_account_periodic_value_type_0.py +44 -0
- ynab_cli/adapters/ynab/models/month_detail.py +168 -0
- ynab_cli/adapters/ynab/models/month_detail_response.py +65 -0
- ynab_cli/adapters/ynab/models/month_detail_response_data.py +65 -0
- ynab_cli/adapters/ynab/models/month_summaries_response.py +65 -0
- ynab_cli/adapters/ynab/models/month_summaries_response_data.py +81 -0
- ynab_cli/adapters/ynab/models/month_summary.py +145 -0
- ynab_cli/adapters/ynab/models/new_transaction.py +352 -0
- ynab_cli/adapters/ynab/models/patch_category_wrapper.py +65 -0
- ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +65 -0
- ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +65 -0
- ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +73 -0
- ynab_cli/adapters/ynab/models/payee.py +100 -0
- ynab_cli/adapters/ynab/models/payee_location.py +93 -0
- ynab_cli/adapters/ynab/models/payee_location_response.py +65 -0
- ynab_cli/adapters/ynab/models/payee_location_response_data.py +65 -0
- ynab_cli/adapters/ynab/models/payee_locations_response.py +65 -0
- ynab_cli/adapters/ynab/models/payee_locations_response_data.py +73 -0
- ynab_cli/adapters/ynab/models/payee_response.py +65 -0
- ynab_cli/adapters/ynab/models/payee_response_data.py +65 -0
- ynab_cli/adapters/ynab/models/payees_response.py +65 -0
- ynab_cli/adapters/ynab/models/payees_response_data.py +81 -0
- ynab_cli/adapters/ynab/models/post_account_wrapper.py +65 -0
- ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +65 -0
- ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +91 -0
- ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +65 -0
- ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +65 -0
- ynab_cli/adapters/ynab/models/save_account.py +77 -0
- ynab_cli/adapters/ynab/models/save_category.py +129 -0
- ynab_cli/adapters/ynab/models/save_category_response.py +65 -0
- ynab_cli/adapters/ynab/models/save_category_response_data.py +73 -0
- ynab_cli/adapters/ynab/models/save_month_category.py +59 -0
- ynab_cli/adapters/ynab/models/save_payee.py +59 -0
- ynab_cli/adapters/ynab/models/save_payee_response.py +65 -0
- ynab_cli/adapters/ynab/models/save_payee_response_data.py +73 -0
- ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +269 -0
- ynab_cli/adapters/ynab/models/save_sub_transaction.py +166 -0
- ynab_cli/adapters/ynab/models/save_transaction_with_id_or_import_id.py +366 -0
- ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +322 -0
- ynab_cli/adapters/ynab/models/save_transactions_response.py +65 -0
- ynab_cli/adapters/ynab/models/save_transactions_response_data.py +123 -0
- ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +238 -0
- ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +381 -0
- ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +23 -0
- ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +65 -0
- ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +65 -0
- ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +310 -0
- ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +23 -0
- ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +65 -0
- ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +81 -0
- ynab_cli/adapters/ynab/models/sub_transaction.py +259 -0
- ynab_cli/adapters/ynab/models/transaction_cleared_status.py +13 -0
- ynab_cli/adapters/ynab/models/transaction_detail.py +570 -0
- ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +16 -0
- ynab_cli/adapters/ynab/models/transaction_flag_color_type_2_type_1.py +16 -0
- ynab_cli/adapters/ynab/models/transaction_flag_color_type_3_type_1.py +16 -0
- ynab_cli/adapters/ynab/models/transaction_response.py +65 -0
- ynab_cli/adapters/ynab/models/transaction_response_data.py +65 -0
- ynab_cli/adapters/ynab/models/transaction_summary.py +499 -0
- ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_1.py +18 -0
- ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_2_type_1.py +18 -0
- ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_3_type_1.py +18 -0
- ynab_cli/adapters/ynab/models/transactions_import_response.py +65 -0
- ynab_cli/adapters/ynab/models/transactions_import_response_data.py +59 -0
- ynab_cli/adapters/ynab/models/transactions_response.py +65 -0
- ynab_cli/adapters/ynab/models/transactions_response_data.py +81 -0
- ynab_cli/adapters/ynab/models/user.py +60 -0
- ynab_cli/adapters/ynab/models/user_response.py +65 -0
- ynab_cli/adapters/ynab/models/user_response_data.py +65 -0
- ynab_cli/adapters/ynab/types.py +46 -0
- ynab_cli/adapters/ynab/util.py +78 -0
- ynab_cli/domain/__init__.py +0 -0
- ynab_cli/domain/constants.py +3 -0
- ynab_cli/domain/models/__init__.py +0 -0
- ynab_cli/domain/models/rules.py +37 -0
- ynab_cli/domain/ports/__init__.py +5 -0
- ynab_cli/domain/ports/io.py +63 -0
- ynab_cli/domain/settings.py +16 -0
- ynab_cli/domain/use_cases/__init__.py +0 -0
- ynab_cli/domain/use_cases/categories.py +105 -0
- ynab_cli/domain/use_cases/payees.py +232 -0
- ynab_cli/domain/use_cases/transactions.py +93 -0
- ynab_cli/host/__init__.py +14 -0
- ynab_cli/host/cli.py +37 -0
- ynab_cli/host/click/__init__.py +0 -0
- ynab_cli/host/click/cli.py +44 -0
- ynab_cli/host/click/commands/__init__.py +0 -0
- ynab_cli/host/click/commands/categories.py +92 -0
- ynab_cli/host/click/commands/payees.py +157 -0
- ynab_cli/host/click/commands/rich/__init__.py +0 -0
- ynab_cli/host/click/commands/rich/progress_table.py +37 -0
- ynab_cli/host/click/commands/transactions.py +76 -0
- ynab_cli/host/constants.py +3 -0
- ynab_cli/host/textual/__init__.py +0 -0
- ynab_cli/host/textual/app.py +98 -0
- ynab_cli/host/textual/app.tcss +47 -0
- ynab_cli/host/textual/cli.py +42 -0
- ynab_cli/host/textual/widgets/__init__.py +58 -0
- ynab_cli/host/textual/widgets/categories.py +90 -0
- ynab_cli/host/textual/widgets/payees.py +154 -0
- ynab_cli/host/textual/widgets/transactions.py +89 -0
- ynab_cli/py.typed +0 -0
- ynab_cli-0.3.0.dist-info/METADATA +102 -0
- ynab_cli-0.3.0.dist-info/RECORD +206 -0
- ynab_cli-0.3.0.dist-info/WHEEL +4 -0
- ynab_cli-0.3.0.dist-info/entry_points.txt +2 -0
- ynab_cli-0.3.0.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any, TypeVar, cast
|
|
3
|
+
|
|
4
|
+
from attrs import define as _attrs_define
|
|
5
|
+
from attrs import field as _attrs_field
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T", bound="TransactionsImportResponseData")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@_attrs_define
|
|
11
|
+
class TransactionsImportResponseData:
|
|
12
|
+
"""
|
|
13
|
+
Attributes:
|
|
14
|
+
transaction_ids (list[str]): The list of transaction ids that were imported.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
transaction_ids: list[str]
|
|
18
|
+
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict[str, Any]:
|
|
21
|
+
transaction_ids = self.transaction_ids
|
|
22
|
+
|
|
23
|
+
field_dict: dict[str, Any] = {}
|
|
24
|
+
field_dict.update(self.additional_properties)
|
|
25
|
+
field_dict.update(
|
|
26
|
+
{
|
|
27
|
+
"transaction_ids": transaction_ids,
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return field_dict
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
35
|
+
d = dict(src_dict)
|
|
36
|
+
transaction_ids = cast(list[str], d.pop("transaction_ids"))
|
|
37
|
+
|
|
38
|
+
transactions_import_response_data = cls(
|
|
39
|
+
transaction_ids=transaction_ids,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
transactions_import_response_data.additional_properties = d
|
|
43
|
+
return transactions_import_response_data
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def additional_keys(self) -> list[str]:
|
|
47
|
+
return list(self.additional_properties.keys())
|
|
48
|
+
|
|
49
|
+
def __getitem__(self, key: str) -> Any:
|
|
50
|
+
return self.additional_properties[key]
|
|
51
|
+
|
|
52
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
53
|
+
self.additional_properties[key] = value
|
|
54
|
+
|
|
55
|
+
def __delitem__(self, key: str) -> None:
|
|
56
|
+
del self.additional_properties[key]
|
|
57
|
+
|
|
58
|
+
def __contains__(self, key: str) -> bool:
|
|
59
|
+
return key in self.additional_properties
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
3
|
+
|
|
4
|
+
from attrs import define as _attrs_define
|
|
5
|
+
from attrs import field as _attrs_field
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ynab_cli.adapters.ynab.models.transactions_response_data import TransactionsResponseData
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound="TransactionsResponse")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@_attrs_define
|
|
15
|
+
class TransactionsResponse:
|
|
16
|
+
"""
|
|
17
|
+
Attributes:
|
|
18
|
+
data (TransactionsResponseData):
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
data: "TransactionsResponseData"
|
|
22
|
+
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict[str, Any]:
|
|
25
|
+
data = self.data.to_dict()
|
|
26
|
+
|
|
27
|
+
field_dict: dict[str, Any] = {}
|
|
28
|
+
field_dict.update(self.additional_properties)
|
|
29
|
+
field_dict.update(
|
|
30
|
+
{
|
|
31
|
+
"data": data,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return field_dict
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
39
|
+
from ynab_cli.adapters.ynab.models.transactions_response_data import TransactionsResponseData
|
|
40
|
+
|
|
41
|
+
d = dict(src_dict)
|
|
42
|
+
data = TransactionsResponseData.from_dict(d.pop("data"))
|
|
43
|
+
|
|
44
|
+
transactions_response = cls(
|
|
45
|
+
data=data,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
transactions_response.additional_properties = d
|
|
49
|
+
return transactions_response
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def additional_keys(self) -> list[str]:
|
|
53
|
+
return list(self.additional_properties.keys())
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key: str) -> Any:
|
|
56
|
+
return self.additional_properties[key]
|
|
57
|
+
|
|
58
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
59
|
+
self.additional_properties[key] = value
|
|
60
|
+
|
|
61
|
+
def __delitem__(self, key: str) -> None:
|
|
62
|
+
del self.additional_properties[key]
|
|
63
|
+
|
|
64
|
+
def __contains__(self, key: str) -> bool:
|
|
65
|
+
return key in self.additional_properties
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
3
|
+
|
|
4
|
+
from attrs import define as _attrs_define
|
|
5
|
+
from attrs import field as _attrs_field
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ynab_cli.adapters.ynab.models.transaction_detail import TransactionDetail
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound="TransactionsResponseData")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@_attrs_define
|
|
15
|
+
class TransactionsResponseData:
|
|
16
|
+
"""
|
|
17
|
+
Attributes:
|
|
18
|
+
transactions (list['TransactionDetail']):
|
|
19
|
+
server_knowledge (int): The knowledge of the server
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
transactions: list["TransactionDetail"]
|
|
23
|
+
server_knowledge: int
|
|
24
|
+
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, Any]:
|
|
27
|
+
transactions = []
|
|
28
|
+
for transactions_item_data in self.transactions:
|
|
29
|
+
transactions_item = transactions_item_data.to_dict()
|
|
30
|
+
transactions.append(transactions_item)
|
|
31
|
+
|
|
32
|
+
server_knowledge = self.server_knowledge
|
|
33
|
+
|
|
34
|
+
field_dict: dict[str, Any] = {}
|
|
35
|
+
field_dict.update(self.additional_properties)
|
|
36
|
+
field_dict.update(
|
|
37
|
+
{
|
|
38
|
+
"transactions": transactions,
|
|
39
|
+
"server_knowledge": server_knowledge,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return field_dict
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
47
|
+
from ynab_cli.adapters.ynab.models.transaction_detail import TransactionDetail
|
|
48
|
+
|
|
49
|
+
d = dict(src_dict)
|
|
50
|
+
transactions = []
|
|
51
|
+
_transactions = d.pop("transactions")
|
|
52
|
+
for transactions_item_data in _transactions:
|
|
53
|
+
transactions_item = TransactionDetail.from_dict(transactions_item_data)
|
|
54
|
+
|
|
55
|
+
transactions.append(transactions_item)
|
|
56
|
+
|
|
57
|
+
server_knowledge = d.pop("server_knowledge")
|
|
58
|
+
|
|
59
|
+
transactions_response_data = cls(
|
|
60
|
+
transactions=transactions,
|
|
61
|
+
server_knowledge=server_knowledge,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
transactions_response_data.additional_properties = d
|
|
65
|
+
return transactions_response_data
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def additional_keys(self) -> list[str]:
|
|
69
|
+
return list(self.additional_properties.keys())
|
|
70
|
+
|
|
71
|
+
def __getitem__(self, key: str) -> Any:
|
|
72
|
+
return self.additional_properties[key]
|
|
73
|
+
|
|
74
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
75
|
+
self.additional_properties[key] = value
|
|
76
|
+
|
|
77
|
+
def __delitem__(self, key: str) -> None:
|
|
78
|
+
del self.additional_properties[key]
|
|
79
|
+
|
|
80
|
+
def __contains__(self, key: str) -> bool:
|
|
81
|
+
return key in self.additional_properties
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any, TypeVar
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from attrs import define as _attrs_define
|
|
6
|
+
from attrs import field as _attrs_field
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T", bound="User")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@_attrs_define
|
|
12
|
+
class User:
|
|
13
|
+
"""
|
|
14
|
+
Attributes:
|
|
15
|
+
id (UUID):
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
id: UUID
|
|
19
|
+
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict[str, Any]:
|
|
22
|
+
id = str(self.id)
|
|
23
|
+
|
|
24
|
+
field_dict: dict[str, Any] = {}
|
|
25
|
+
field_dict.update(self.additional_properties)
|
|
26
|
+
field_dict.update(
|
|
27
|
+
{
|
|
28
|
+
"id": id,
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return field_dict
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
36
|
+
d = dict(src_dict)
|
|
37
|
+
id = UUID(d.pop("id"))
|
|
38
|
+
|
|
39
|
+
user = cls(
|
|
40
|
+
id=id,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
user.additional_properties = d
|
|
44
|
+
return user
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def additional_keys(self) -> list[str]:
|
|
48
|
+
return list(self.additional_properties.keys())
|
|
49
|
+
|
|
50
|
+
def __getitem__(self, key: str) -> Any:
|
|
51
|
+
return self.additional_properties[key]
|
|
52
|
+
|
|
53
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
54
|
+
self.additional_properties[key] = value
|
|
55
|
+
|
|
56
|
+
def __delitem__(self, key: str) -> None:
|
|
57
|
+
del self.additional_properties[key]
|
|
58
|
+
|
|
59
|
+
def __contains__(self, key: str) -> bool:
|
|
60
|
+
return key in self.additional_properties
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
3
|
+
|
|
4
|
+
from attrs import define as _attrs_define
|
|
5
|
+
from attrs import field as _attrs_field
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ynab_cli.adapters.ynab.models.user_response_data import UserResponseData
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound="UserResponse")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@_attrs_define
|
|
15
|
+
class UserResponse:
|
|
16
|
+
"""
|
|
17
|
+
Attributes:
|
|
18
|
+
data (UserResponseData):
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
data: "UserResponseData"
|
|
22
|
+
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict[str, Any]:
|
|
25
|
+
data = self.data.to_dict()
|
|
26
|
+
|
|
27
|
+
field_dict: dict[str, Any] = {}
|
|
28
|
+
field_dict.update(self.additional_properties)
|
|
29
|
+
field_dict.update(
|
|
30
|
+
{
|
|
31
|
+
"data": data,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return field_dict
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
39
|
+
from ynab_cli.adapters.ynab.models.user_response_data import UserResponseData
|
|
40
|
+
|
|
41
|
+
d = dict(src_dict)
|
|
42
|
+
data = UserResponseData.from_dict(d.pop("data"))
|
|
43
|
+
|
|
44
|
+
user_response = cls(
|
|
45
|
+
data=data,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
user_response.additional_properties = d
|
|
49
|
+
return user_response
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def additional_keys(self) -> list[str]:
|
|
53
|
+
return list(self.additional_properties.keys())
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key: str) -> Any:
|
|
56
|
+
return self.additional_properties[key]
|
|
57
|
+
|
|
58
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
59
|
+
self.additional_properties[key] = value
|
|
60
|
+
|
|
61
|
+
def __delitem__(self, key: str) -> None:
|
|
62
|
+
del self.additional_properties[key]
|
|
63
|
+
|
|
64
|
+
def __contains__(self, key: str) -> bool:
|
|
65
|
+
return key in self.additional_properties
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
3
|
+
|
|
4
|
+
from attrs import define as _attrs_define
|
|
5
|
+
from attrs import field as _attrs_field
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ynab_cli.adapters.ynab.models.user import User
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound="UserResponseData")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@_attrs_define
|
|
15
|
+
class UserResponseData:
|
|
16
|
+
"""
|
|
17
|
+
Attributes:
|
|
18
|
+
user (User):
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
user: "User"
|
|
22
|
+
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict[str, Any]:
|
|
25
|
+
user = self.user.to_dict()
|
|
26
|
+
|
|
27
|
+
field_dict: dict[str, Any] = {}
|
|
28
|
+
field_dict.update(self.additional_properties)
|
|
29
|
+
field_dict.update(
|
|
30
|
+
{
|
|
31
|
+
"user": user,
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return field_dict
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
39
|
+
from ynab_cli.adapters.ynab.models.user import User
|
|
40
|
+
|
|
41
|
+
d = dict(src_dict)
|
|
42
|
+
user = User.from_dict(d.pop("user"))
|
|
43
|
+
|
|
44
|
+
user_response_data = cls(
|
|
45
|
+
user=user,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
user_response_data.additional_properties = d
|
|
49
|
+
return user_response_data
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def additional_keys(self) -> list[str]:
|
|
53
|
+
return list(self.additional_properties.keys())
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key: str) -> Any:
|
|
56
|
+
return self.additional_properties[key]
|
|
57
|
+
|
|
58
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
59
|
+
self.additional_properties[key] = value
|
|
60
|
+
|
|
61
|
+
def __delitem__(self, key: str) -> None:
|
|
62
|
+
del self.additional_properties[key]
|
|
63
|
+
|
|
64
|
+
def __contains__(self, key: str) -> bool:
|
|
65
|
+
return key in self.additional_properties
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Contains some shared types for properties"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import MutableMapping
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import BinaryIO, Generic, Literal, TypeVar
|
|
6
|
+
|
|
7
|
+
from attrs import define
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Unset:
|
|
11
|
+
def __bool__(self) -> Literal[False]:
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
UNSET: Unset = Unset()
|
|
16
|
+
|
|
17
|
+
FileJsonType = tuple[str | None, BinaryIO, str | None]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@define
|
|
21
|
+
class File:
|
|
22
|
+
"""Contains information for file uploads"""
|
|
23
|
+
|
|
24
|
+
payload: BinaryIO
|
|
25
|
+
file_name: str | None = None
|
|
26
|
+
mime_type: str | None = None
|
|
27
|
+
|
|
28
|
+
def to_tuple(self) -> FileJsonType:
|
|
29
|
+
"""Return a tuple representation that httpx will accept for multipart/form-data"""
|
|
30
|
+
return self.file_name, self.payload, self.mime_type
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
T = TypeVar("T")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@define
|
|
37
|
+
class Response(Generic[T]):
|
|
38
|
+
"""A response from an endpoint"""
|
|
39
|
+
|
|
40
|
+
status_code: HTTPStatus
|
|
41
|
+
content: bytes
|
|
42
|
+
headers: MutableMapping[str, str]
|
|
43
|
+
parsed: T | None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = ["UNSET", "File", "FileJsonType", "Response", "Unset"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from collections.abc import Callable, Coroutine
|
|
2
|
+
from typing import Any, ParamSpec, TypeVar
|
|
3
|
+
|
|
4
|
+
from ynab_cli.adapters.ynab import AuthenticatedClient
|
|
5
|
+
from ynab_cli.domain.ports.io import IO
|
|
6
|
+
|
|
7
|
+
from .models import ErrorDetail, ErrorResponse
|
|
8
|
+
from .types import Response
|
|
9
|
+
|
|
10
|
+
P = ParamSpec("P")
|
|
11
|
+
R = TypeVar("R")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApiError(Exception):
|
|
15
|
+
"""Raised by API functions when the response status is not 200 OK and Client.raise_on_unexpected_status is True"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, status_code: int, error_detail: ErrorDetail | None = None):
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.error_detail = error_detail
|
|
20
|
+
|
|
21
|
+
super().__init__(f"Unexpected status code: {status_code}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_parsed_response_data(response: Response[ErrorResponse | R]) -> R | None:
|
|
25
|
+
parsed = response.parsed
|
|
26
|
+
|
|
27
|
+
if isinstance(parsed, ErrorResponse):
|
|
28
|
+
error_detail = parsed.error
|
|
29
|
+
raise ApiError(status_code=response.status_code, error_detail=error_detail)
|
|
30
|
+
|
|
31
|
+
if response.status_code >= 400:
|
|
32
|
+
error_detail = ErrorDetail(
|
|
33
|
+
id=str(response.status_code.value),
|
|
34
|
+
name="unknown_error",
|
|
35
|
+
detail="An unknown error occurred",
|
|
36
|
+
)
|
|
37
|
+
raise ApiError(status_code=response.status_code, error_detail=error_detail)
|
|
38
|
+
|
|
39
|
+
return parsed
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def run_asyncio_detailed(
|
|
43
|
+
io: IO,
|
|
44
|
+
asyncio_detailed: Callable[P, Coroutine[Any, Any, Response[ErrorResponse | R]]],
|
|
45
|
+
*args: P.args,
|
|
46
|
+
**kwargs: P.kwargs,
|
|
47
|
+
) -> R | None:
|
|
48
|
+
try:
|
|
49
|
+
client = kwargs["client"] if "client" in kwargs and isinstance(kwargs["client"], AuthenticatedClient) else None
|
|
50
|
+
|
|
51
|
+
response = await asyncio_detailed(*args, **kwargs)
|
|
52
|
+
parsed_data = get_parsed_response_data(response)
|
|
53
|
+
except ApiError as e:
|
|
54
|
+
if e.status_code == 429 and client is not None:
|
|
55
|
+
new_access_token = await io.prompt(
|
|
56
|
+
prompt="API rate limit exceeded. Enter a new access token", password=True
|
|
57
|
+
)
|
|
58
|
+
client.token = new_access_token
|
|
59
|
+
response = await asyncio_detailed(*args, **kwargs)
|
|
60
|
+
parsed_data = get_parsed_response_data(response)
|
|
61
|
+
else:
|
|
62
|
+
raise e
|
|
63
|
+
|
|
64
|
+
return parsed_data
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def get_asyncio_detailed(
|
|
68
|
+
io: IO,
|
|
69
|
+
asyncio_detailed: Callable[P, Coroutine[Any, Any, Response[ErrorResponse | R]]],
|
|
70
|
+
*args: P.args,
|
|
71
|
+
**kwargs: P.kwargs,
|
|
72
|
+
) -> R:
|
|
73
|
+
response = await run_asyncio_detailed(io, asyncio_detailed, *args, **kwargs)
|
|
74
|
+
if response is None:
|
|
75
|
+
raise ApiError(
|
|
76
|
+
status_code=500, error_detail=ErrorDetail(id="500", name="internal_error", detail="No response data")
|
|
77
|
+
)
|
|
78
|
+
return response
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from attrs import define as _attrs_define
|
|
4
|
+
|
|
5
|
+
from ynab_cli.adapters.ynab import models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@_attrs_define
|
|
9
|
+
class TransactionRule:
|
|
10
|
+
# List of rules that follow the rule-engine format for a transaction.
|
|
11
|
+
# See: https://zerosteiner.github.io/rule-engine/
|
|
12
|
+
# The rules are given a TransactionDetail object
|
|
13
|
+
# See: https://api.ynab.com/v1#/Transactions/getTransactions
|
|
14
|
+
rules: list[str]
|
|
15
|
+
|
|
16
|
+
# The JSON SaveTransactionWithIdOrImportId to apply to the transaction if the rules are matched.
|
|
17
|
+
# The id/import_id will be provided. Just fill in what you want to update.
|
|
18
|
+
# See: https://api.ynab.com/v1#/Transactions/updateTransactions
|
|
19
|
+
patch: models.SaveTransactionWithIdOrImportId | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@_attrs_define
|
|
23
|
+
class TransactionRules:
|
|
24
|
+
transaction_rules: list[TransactionRule]
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_dict(cls, data: dict[str, Any]) -> "TransactionRules":
|
|
28
|
+
transaction_rules = []
|
|
29
|
+
for rule in data.get("transaction_rules", []):
|
|
30
|
+
rules = [str(r) for r in rule.get("rules", [])]
|
|
31
|
+
patch = None
|
|
32
|
+
if "patch" in rule:
|
|
33
|
+
patch = models.SaveTransactionWithIdOrImportId.from_dict(rule["patch"])
|
|
34
|
+
|
|
35
|
+
transaction_rules.append(TransactionRule(rules=rules, patch=patch))
|
|
36
|
+
|
|
37
|
+
return cls(transaction_rules=transaction_rules)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from getpass import getpass
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
from typing_extensions import override
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Progress(Protocol):
|
|
8
|
+
async def update(
|
|
9
|
+
self, *, total: float | None = None, completed: float | None = None, advance: float | None = None
|
|
10
|
+
) -> None: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IO(Protocol):
|
|
14
|
+
progress: Progress
|
|
15
|
+
|
|
16
|
+
async def prompt(self, prompt: str, password: bool = False) -> str: ...
|
|
17
|
+
|
|
18
|
+
async def print(self, message: str) -> None: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StdProgress(Progress):
|
|
22
|
+
def __init__(self, io: "StdIO") -> None:
|
|
23
|
+
self._io = io
|
|
24
|
+
self._total: float | None = None
|
|
25
|
+
self._completed: float | None = None
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
async def update(
|
|
29
|
+
self, *, total: float | None = None, completed: float | None = None, advance: float | None = None
|
|
30
|
+
) -> None:
|
|
31
|
+
if total is not None:
|
|
32
|
+
self._total = total
|
|
33
|
+
if completed is not None:
|
|
34
|
+
self._completed = completed
|
|
35
|
+
if advance is not None:
|
|
36
|
+
if self._completed is None:
|
|
37
|
+
self._completed = 0
|
|
38
|
+
self._completed += advance
|
|
39
|
+
|
|
40
|
+
if self._total is not None and self._completed is not None:
|
|
41
|
+
await self._io.print(f"Progress: {self._completed}/{self._total}")
|
|
42
|
+
elif self._total is not None:
|
|
43
|
+
await self._io.print(f"Progress: {self._total} to go")
|
|
44
|
+
elif self._completed is not None:
|
|
45
|
+
await self._io.print(f"Progress: {self._completed} completed")
|
|
46
|
+
else:
|
|
47
|
+
await self._io.print("No progress information available")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class StdIO(IO):
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.progress = StdProgress(self)
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
async def prompt(self, prompt: str, password: bool = False) -> str:
|
|
56
|
+
if password:
|
|
57
|
+
return getpass(prompt)
|
|
58
|
+
else:
|
|
59
|
+
return input(prompt)
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
async def print(self, message: str) -> None:
|
|
63
|
+
print(message)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from attrs import define as _attrs_define
|
|
2
|
+
from attrs import field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@_attrs_define
|
|
6
|
+
class YnabSettings:
|
|
7
|
+
access_token: str = ""
|
|
8
|
+
budget_id: str = ""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@_attrs_define
|
|
12
|
+
class Settings:
|
|
13
|
+
dry_run: bool = False
|
|
14
|
+
debug: bool = False
|
|
15
|
+
|
|
16
|
+
ynab: YnabSettings = field(factory=YnabSettings)
|
|
File without changes
|