From 49afff36881b611997cfbc98c4a40ea889573311 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Mon, 1 Dec 2025 14:32:52 +0100 Subject: [PATCH 1/5] Add fetching additional DataSource fields --- README.md | 15 +++- chartmogul/api/data_source.py | 50 +++++++++++++- test/api/test_data_source.py | 125 +++++++++++++++++++++++++++++++--- 3 files changed, 177 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4ddbe54..041b52a 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,19 @@ Available methods in Import API: ```python chartmogul.DataSource.create(config, data={'name': 'In-house billing'}) -chartmogul.DataSource.retrieve(config, uuid='ds_5915ee5a-babd-406b-b8ce-d207133fb4cb') -chartmogul.DataSource.all(config) +chartmogul.DataSource.retrieve( + config, + uuid='ds_5915ee5a-babd-406b-b8ce-d207133fb4cb', + with_processing_status=True, + with_auto_churn_subscription_setting=True, + with_invoice_handling_setting=True +) +chartmogul.DataSource.all( + config, + with_processing_status=True, + with_auto_churn_subscription_setting=True, + with_invoice_handling_setting=True +) chartmogul.DataSource.destroy(config, uuid='ds_5915ee5a-babd-406b-b8ce-d207133fb4cb') ``` diff --git a/chartmogul/api/data_source.py b/chartmogul/api/data_source.py index eadce4f..fba8492 100644 --- a/chartmogul/api/data_source.py +++ b/chartmogul/api/data_source.py @@ -1,8 +1,42 @@ from marshmallow import Schema, fields, post_load, EXCLUDE -from ..resource import Resource +from ..resource import Resource, DataObject from collections import namedtuple +class ProcessingStatus(DataObject): + class _Schema(Schema): + processed = fields.Integer(allow_none=True) + pending = fields.Integer(allow_none=True) + failed = fields.Integer(allow_none=True) + + @post_load + def make(self, data, **kwargs): + return ProcessingStatus(**data) + + +class InvoiceHandlingMethod(DataObject): + class _Schema(Schema): + create_subscription_when_invoice_is = fields.String() + update_subscription_when_invoice_is = fields.String() + prevent_subscription_for_invoice_voided = fields.Boolean() + prevent_subscription_for_invoice_refunded = fields.Boolean() + prevent_subscription_for_invoice_written_off = fields.Boolean() + + @post_load + def make(self, data, **kwargs): + return InvoiceHandlingMethod(**data) + + +class InvoiceHandlingSetting(DataObject): + class _Schema(Schema): + manual = fields.Nested(InvoiceHandlingMethod._Schema, many=False, unknown=EXCLUDE) + automatic = fields.Nested(InvoiceHandlingMethod._Schema, many=False, unknown=EXCLUDE) + + @post_load + def make(self, data, **kwargs): + return InvoiceHandlingSetting(**data) + + class DataSource(Resource): """ https://dev.chartmogul.com/v1.0/reference#data-sources @@ -10,7 +44,16 @@ class DataSource(Resource): _path = "/data_sources{/uuid}" _root_key = "data_sources" - _many = namedtuple("DataSources", [_root_key]) + _many = namedtuple( + "DataSources", + [ + _root_key, + "with_processing_status", + "with_auto_churn_subscription_setting", + "with_invoice_handling_setting" + ], + defaults=[None, None, None] + ) class _Schema(Schema): uuid = fields.String() @@ -18,6 +61,9 @@ class _Schema(Schema): created_at = fields.DateTime() status = fields.Str() system = fields.Str() + processing_status = fields.Nested(ProcessingStatus._Schema, many=False, unknown=EXCLUDE, allow_none=True) + auto_churn_subscription_setting = fields.Boolean(allow_none=True) + invoice_handling_setting = fields.Nested(InvoiceHandlingSetting._Schema, many=False, unknown=EXCLUDE, allow_none=True) @post_load def make(self, data, **kwargs): diff --git a/test/api/test_data_source.py b/test/api/test_data_source.py index 902ad98..a9661d7 100644 --- a/test/api/test_data_source.py +++ b/test/api/test_data_source.py @@ -22,7 +22,7 @@ def test_create(self, mock_requests): "name": "test", "uuid": "my_uuid", "created_at": "2016-01-10T15:34:05.144Z", - "status": "never_imported", + "status": "idle", }, ) @@ -47,23 +47,77 @@ def test_retrieve(self, mock_requests): "name": "test", "uuid": "my_uuid", "created_at": "2016-01-10T15:34:05Z", - "status": "never_imported", + "status": "idle", + "processing_status": { + "processed": 61, + "failed": 3, + "pending": 8 + }, + "auto_churn_subscription_setting": True, + "invoice_handling_setting": { + "manual": { + "create_subscription_when_invoice_is": "open", + "update_subscription_when_invoice_is": "open", + "prevent_subscription_for_invoice_voided": True, + "prevent_subscription_for_invoice_refunded": False, + "prevent_subscription_for_invoice_written_off": True + }, + "automatic": { + "create_subscription_when_invoice_is": "open", + "update_subscription_when_invoice_is": "open", + "prevent_subscription_for_invoice_voided": True, + "prevent_subscription_for_invoice_refunded": False, + "prevent_subscription_for_invoice_written_off": True + } + } }, ) config = Config("token") - ds = DataSource.retrieve(config, uuid="my_uuid").get() + ds = DataSource.retrieve( + config, + uuid="my_uuid", + with_processing_status=True, + with_auto_churn_subscription_setting=True, + with_invoice_handling_setting=True + ).get() expected = DataSource( **{ "name": "test", "uuid": "my_uuid", "created_at": datetime(2016, 1, 10, 15, 34, 5), - "status": "never_imported", + "status": "idle", + "processing_status": { + "processed": 61, + "failed": 3, + "pending": 8 + }, + "auto_churn_subscription_setting": True, + "invoice_handling_setting": { + "manual": { + "create_subscription_when_invoice_is": "open", + "update_subscription_when_invoice_is": "open", + "prevent_subscription_for_invoice_voided": True, + "prevent_subscription_for_invoice_refunded": False, + "prevent_subscription_for_invoice_written_off": True + }, + "automatic": { + "create_subscription_when_invoice_is": "open", + "update_subscription_when_invoice_is": "open", + "prevent_subscription_for_invoice_voided": True, + "prevent_subscription_for_invoice_refunded": False, + "prevent_subscription_for_invoice_written_off": True + } + } } ) self.assertEqual(mock_requests.call_count, 1, "expected call") - self.assertEqual(mock_requests.last_request.qs, {}) + self.assertEqual(mock_requests.last_request.qs, { + "with_processing_status": ["true"], + "with_auto_churn_subscription_setting": ["true"], + "with_invoice_handling_setting": ["true"] + }) self.assertEqual(mock_requests.last_request.text, None) self.assertTrue(isinstance(ds, DataSource)) self.assertTrue(isinstance(ds.created_at, datetime)) @@ -81,14 +135,41 @@ def test_all(self, mock_requests): "name": "test", "uuid": "my_uuid", "created_at": "2016-01-10T15:34:05Z", - "status": "never_imported", + "status": "idle", + "processing_status": { + "processed": 61, + "failed": 3, + "pending": 8 + }, + "auto_churn_subscription_setting": True, + "invoice_handling_setting": { + "manual": { + "create_subscription_when_invoice_is": "open", + "update_subscription_when_invoice_is": "open", + "prevent_subscription_for_invoice_voided": True, + "prevent_subscription_for_invoice_refunded": False, + "prevent_subscription_for_invoice_written_off": True + }, + "automatic": { + "create_subscription_when_invoice_is": "open", + "update_subscription_when_invoice_is": "open", + "prevent_subscription_for_invoice_voided": True, + "prevent_subscription_for_invoice_refunded": False, + "prevent_subscription_for_invoice_written_off": True + } + } } ] }, ) config = Config("token") - ds = DataSource.all(config).get() + ds = DataSource.all( + config, + with_processing_status=True, + with_auto_churn_subscription_setting=True, + with_invoice_handling_setting=True + ).get() expected = DataSource._many( data_sources=[ DataSource( @@ -96,14 +177,40 @@ def test_all(self, mock_requests): "name": "test", "uuid": "my_uuid", "created_at": datetime(2016, 1, 10, 15, 34, 5), - "status": "never_imported", + "status": "idle", + "processing_status": { + "processed": 61, + "failed": 3, + "pending": 8 + }, + "auto_churn_subscription_setting": True, + "invoice_handling_setting": { + "manual": { + "create_subscription_when_invoice_is": "open", + "update_subscription_when_invoice_is": "open", + "prevent_subscription_for_invoice_voided": True, + "prevent_subscription_for_invoice_refunded": False, + "prevent_subscription_for_invoice_written_off": True + }, + "automatic": { + "create_subscription_when_invoice_is": "open", + "update_subscription_when_invoice_is": "open", + "prevent_subscription_for_invoice_voided": True, + "prevent_subscription_for_invoice_refunded": False, + "prevent_subscription_for_invoice_written_off": True + } + } } ) ] ) self.assertEqual(mock_requests.call_count, 1, "expected call") - self.assertEqual(mock_requests.last_request.qs, {}) + self.assertEqual(mock_requests.last_request.qs, { + "with_processing_status": ["true"], + "with_auto_churn_subscription_setting": ["true"], + "with_invoice_handling_setting": ["true"] + }) self.assertEqual(mock_requests.last_request.text, None) self.assertTrue(isinstance(ds.data_sources[0], DataSource)) From 5d67feda6dc79e2579920588533386e81bc4d46a Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 9 Dec 2025 10:13:49 +0100 Subject: [PATCH 2/5] Update auto_churn_subscription_setting response format --- chartmogul/api/data_source.py | 12 +++++++++++- test/api/test_data_source.py | 20 ++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/chartmogul/api/data_source.py b/chartmogul/api/data_source.py index fba8492..414e531 100644 --- a/chartmogul/api/data_source.py +++ b/chartmogul/api/data_source.py @@ -37,6 +37,16 @@ def make(self, data, **kwargs): return InvoiceHandlingSetting(**data) +class AutoChurnSubscriptionSetting(DataObject): + class _Schema(Schema): + enabled = fields.Boolean() + interval = fields.Integer(allow_none=True) + + @post_load + def make(self, data, **kwargs): + return AutoChurnSubscriptionSetting(**data) + + class DataSource(Resource): """ https://dev.chartmogul.com/v1.0/reference#data-sources @@ -62,7 +72,7 @@ class _Schema(Schema): status = fields.Str() system = fields.Str() processing_status = fields.Nested(ProcessingStatus._Schema, many=False, unknown=EXCLUDE, allow_none=True) - auto_churn_subscription_setting = fields.Boolean(allow_none=True) + auto_churn_subscription_setting = fields.Nested(AutoChurnSubscriptionSetting._Schema, many=False, unknown=EXCLUDE, allow_none=True) invoice_handling_setting = fields.Nested(InvoiceHandlingSetting._Schema, many=False, unknown=EXCLUDE, allow_none=True) @post_load diff --git a/test/api/test_data_source.py b/test/api/test_data_source.py index a9661d7..620f43d 100644 --- a/test/api/test_data_source.py +++ b/test/api/test_data_source.py @@ -53,7 +53,10 @@ def test_retrieve(self, mock_requests): "failed": 3, "pending": 8 }, - "auto_churn_subscription_setting": True, + "auto_churn_subscription_setting": { + "enabled": True, + "interval": 30 + }, "invoice_handling_setting": { "manual": { "create_subscription_when_invoice_is": "open", @@ -92,7 +95,10 @@ def test_retrieve(self, mock_requests): "failed": 3, "pending": 8 }, - "auto_churn_subscription_setting": True, + "auto_churn_subscription_setting": { + "enabled": True, + "interval": 30 + }, "invoice_handling_setting": { "manual": { "create_subscription_when_invoice_is": "open", @@ -141,7 +147,10 @@ def test_all(self, mock_requests): "failed": 3, "pending": 8 }, - "auto_churn_subscription_setting": True, + "auto_churn_subscription_setting": { + "enabled": True, + "interval": 30 + }, "invoice_handling_setting": { "manual": { "create_subscription_when_invoice_is": "open", @@ -183,7 +192,10 @@ def test_all(self, mock_requests): "failed": 3, "pending": 8 }, - "auto_churn_subscription_setting": True, + "auto_churn_subscription_setting": { + "enabled": True, + "interval": 30 + }, "invoice_handling_setting": { "manual": { "create_subscription_when_invoice_is": "open", From c2bb8e0f63894a5787e922ba7036d8cdd3d0002a Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 11 Dec 2025 14:50:38 +0100 Subject: [PATCH 3/5] Update python bool Trues to be converted to strings --- chartmogul/api/data_source.py | 31 ++++++++++++++++++++++--------- test/api/test_data_source.py | 8 ++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/chartmogul/api/data_source.py b/chartmogul/api/data_source.py index 414e531..eea76e6 100644 --- a/chartmogul/api/data_source.py +++ b/chartmogul/api/data_source.py @@ -54,26 +54,39 @@ class DataSource(Resource): _path = "/data_sources{/uuid}" _root_key = "data_sources" + _bool_query_params = [ + 'with_processing_status', + 'with_auto_churn_subscription_setting', + 'with_invoice_handling_setting' + ] _many = namedtuple( "DataSources", - [ - _root_key, - "with_processing_status", - "with_auto_churn_subscription_setting", - "with_invoice_handling_setting" - ], + [_root_key] + _bool_query_params, defaults=[None, None, None] ) + @classmethod + def _preProcessParams(cls, params): + params = super()._preProcessParams(params) + + for query_param in cls._bool_query_params: + if query_param in params and isinstance(params[query_param], bool): + if params[query_param] == True: + params[query_param] = 'true' + else: + del params[query_param] + + return params + class _Schema(Schema): uuid = fields.String() name = fields.String() created_at = fields.DateTime() status = fields.Str() system = fields.Str() - processing_status = fields.Nested(ProcessingStatus._Schema, many=False, unknown=EXCLUDE, allow_none=True) - auto_churn_subscription_setting = fields.Nested(AutoChurnSubscriptionSetting._Schema, many=False, unknown=EXCLUDE, allow_none=True) - invoice_handling_setting = fields.Nested(InvoiceHandlingSetting._Schema, many=False, unknown=EXCLUDE, allow_none=True) + processing_status = fields.Nested(ProcessingStatus._Schema, many=False, allow_none=True) + auto_churn_subscription_setting = fields.Nested(AutoChurnSubscriptionSetting._Schema, many=False, allow_none=True) + invoice_handling_setting = fields.Nested(InvoiceHandlingSetting._Schema, many=False, allow_none=True) @post_load def make(self, data, **kwargs): diff --git a/test/api/test_data_source.py b/test/api/test_data_source.py index 620f43d..6a14a08 100644 --- a/test/api/test_data_source.py +++ b/test/api/test_data_source.py @@ -128,6 +128,9 @@ def test_retrieve(self, mock_requests): self.assertTrue(isinstance(ds, DataSource)) self.assertTrue(isinstance(ds.created_at, datetime)) self.assertEqual(ds.name, "test") + self.assertEqual(ds.processing_status.processed, 61) + self.assertEqual(ds.auto_churn_subscription_setting.interval, 30) + self.assertEqual(ds.invoice_handling_setting.manual.create_subscription_when_invoice_is, "open") @requests_mock.mock() def test_all(self, mock_requests): @@ -225,6 +228,11 @@ def test_all(self, mock_requests): }) self.assertEqual(mock_requests.last_request.text, None) self.assertTrue(isinstance(ds.data_sources[0], DataSource)) + self.assertTrue(isinstance(ds.data_sources[0].created_at, datetime)) + self.assertEqual(ds.data_sources[0].name, "test") + self.assertEqual(ds.data_sources[0].processing_status.processed, 61) + self.assertEqual(ds.data_sources[0].auto_churn_subscription_setting.interval, 30) + self.assertEqual(ds.data_sources[0].invoice_handling_setting.manual.create_subscription_when_invoice_is, "open") @requests_mock.mock() def test_destroy(self, mock_requests): From dfd09e569c5eeec968dc71c8d05a3214c31f9938 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Thu, 11 Dec 2025 14:53:38 +0100 Subject: [PATCH 4/5] Fix flake8 violation --- chartmogul/api/data_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chartmogul/api/data_source.py b/chartmogul/api/data_source.py index eea76e6..f430138 100644 --- a/chartmogul/api/data_source.py +++ b/chartmogul/api/data_source.py @@ -71,7 +71,7 @@ def _preProcessParams(cls, params): for query_param in cls._bool_query_params: if query_param in params and isinstance(params[query_param], bool): - if params[query_param] == True: + if params[query_param] is True: params[query_param] = 'true' else: del params[query_param] From b7f94c93028fcad2277e34b21f3d565a6b1ea732 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Mon, 15 Dec 2025 12:23:03 +0100 Subject: [PATCH 5/5] Update invoice_handling_setting format to fully backwards-compatible --- chartmogul/api/data_source.py | 25 +------------------------ test/api/test_data_source.py | 4 ++-- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/chartmogul/api/data_source.py b/chartmogul/api/data_source.py index f430138..9e8cb41 100644 --- a/chartmogul/api/data_source.py +++ b/chartmogul/api/data_source.py @@ -14,29 +14,6 @@ def make(self, data, **kwargs): return ProcessingStatus(**data) -class InvoiceHandlingMethod(DataObject): - class _Schema(Schema): - create_subscription_when_invoice_is = fields.String() - update_subscription_when_invoice_is = fields.String() - prevent_subscription_for_invoice_voided = fields.Boolean() - prevent_subscription_for_invoice_refunded = fields.Boolean() - prevent_subscription_for_invoice_written_off = fields.Boolean() - - @post_load - def make(self, data, **kwargs): - return InvoiceHandlingMethod(**data) - - -class InvoiceHandlingSetting(DataObject): - class _Schema(Schema): - manual = fields.Nested(InvoiceHandlingMethod._Schema, many=False, unknown=EXCLUDE) - automatic = fields.Nested(InvoiceHandlingMethod._Schema, many=False, unknown=EXCLUDE) - - @post_load - def make(self, data, **kwargs): - return InvoiceHandlingSetting(**data) - - class AutoChurnSubscriptionSetting(DataObject): class _Schema(Schema): enabled = fields.Boolean() @@ -86,7 +63,7 @@ class _Schema(Schema): system = fields.Str() processing_status = fields.Nested(ProcessingStatus._Schema, many=False, allow_none=True) auto_churn_subscription_setting = fields.Nested(AutoChurnSubscriptionSetting._Schema, many=False, allow_none=True) - invoice_handling_setting = fields.Nested(InvoiceHandlingSetting._Schema, many=False, allow_none=True) + invoice_handling_setting = fields.Raw(allow_none=True) @post_load def make(self, data, **kwargs): diff --git a/test/api/test_data_source.py b/test/api/test_data_source.py index 6a14a08..dad02e8 100644 --- a/test/api/test_data_source.py +++ b/test/api/test_data_source.py @@ -130,7 +130,7 @@ def test_retrieve(self, mock_requests): self.assertEqual(ds.name, "test") self.assertEqual(ds.processing_status.processed, 61) self.assertEqual(ds.auto_churn_subscription_setting.interval, 30) - self.assertEqual(ds.invoice_handling_setting.manual.create_subscription_when_invoice_is, "open") + self.assertEqual(ds.invoice_handling_setting['manual']['create_subscription_when_invoice_is'], "open") @requests_mock.mock() def test_all(self, mock_requests): @@ -232,7 +232,7 @@ def test_all(self, mock_requests): self.assertEqual(ds.data_sources[0].name, "test") self.assertEqual(ds.data_sources[0].processing_status.processed, 61) self.assertEqual(ds.data_sources[0].auto_churn_subscription_setting.interval, 30) - self.assertEqual(ds.data_sources[0].invoice_handling_setting.manual.create_subscription_when_invoice_is, "open") + self.assertEqual(ds.data_sources[0].invoice_handling_setting['manual']['create_subscription_when_invoice_is'], "open") @requests_mock.mock() def test_destroy(self, mock_requests):