Repository: uptick/pymyob

Issue:
Simplify MYOB endpoints file.
There's an awful lot of repetition going on in this file.
Most endpoints follow this same pattern of 5 possible actions:
```
            (ALL, 'Item/', 'Return inventory items for an AccountRight company file.'),
            (GET, 'Item/[uid]/', 'Return selected inventory item.'),
            (PUT, 'Item/[uid]/', 'Update selected inventory items.'),
            (POST, 'Item/', 'Create new inventory item.'),
            (DELETE, 'Item/[uid]/', 'Delete selected inventory item.'),
```
Let's compress each of these into a single entry.

Discussion:

Patch:
diff --git a/myob/api.py b/myob/api.py
index fc066ea..ec02cad 100755
--- a/myob/api.py
+++ b/myob/api.py
@@ -14,7 +14,7 @@ class Myob:
             )
         self.credentials = credentials
         self.companyfiles = CompanyFiles(credentials)
-        self._manager = Manager('', credentials, endpoints=[
+        self._manager = Manager('', credentials, raw_endpoints=[
             (GET, 'Info/', 'Return API build information for each individual endpoint.'),
         ])
 
@@ -28,7 +28,7 @@ class Myob:
 class CompanyFiles:
     def __init__(self, credentials):
         self.credentials = credentials
-        self._manager = Manager('', self.credentials, endpoints=[
+        self._manager = Manager('', self.credentials, raw_endpoints=[
             (ALL, '', 'Return a list of company files.'),
             (GET, '[id]/', 'List endpoints available for a company file.'),
         ])
@@ -56,7 +56,7 @@ class CompanyFile:
         self.data = raw  # Dump remaining raw data here.
         self.credentials = credentials
         for k, v in ENDPOINTS.items():
-            setattr(self, v['plural'], Manager(k, credentials, endpoints=v['methods'], company_id=self.id))
+            setattr(self, v['name'], Manager(k, credentials, endpoints=v['methods'], company_id=self.id))
 
     def __repr__(self):
-        return 'CompanyFile:\n    %s' % '\n    '.join(sorted(v['plural'] for v in ENDPOINTS.values()))
+        return 'CompanyFile:\n    %s' % '\n    '.join(sorted(v['name'] for v in ENDPOINTS.values()))
diff --git a/myob/endpoints.py b/myob/endpoints.py
index 943a830..8c1cda7 100755
--- a/myob/endpoints.py
+++ b/myob/endpoints.py
@@ -1,120 +1,90 @@
+from .utils import pluralise
+
 ALL = 'ALL'
 GET = 'GET'
 POST = 'POST'
 PUT = 'PUT'
 DELETE = 'DELETE'
+CRUD = 'CRUD'  # shorthand for creating the ALL|GET|POST|PUT|DELETE endpoints in one swoop
 
 METHOD_ORDER = [ALL, GET, POST, PUT, DELETE]
 
 ENDPOINTS = {
     'Banking/': {
-        'plural': 'banking',
+        'name': 'banking',
         'methods': [
-            (ALL, '', 'Return all banking types for an AccountRight company file.'),
-            (ALL, 'SpendMoneyTxn/', 'Return all spendmoneytxns for an AccountRight company file.'),
-            (GET, 'SpendMoneyTxn/[uid]/', 'Return selected spendmoneytxn.'),
-            (PUT, 'SpendMoneyTxn/[uid]/', 'Update selected spendmoneytxn.'),
-            (POST, 'SpendMoneyTxn/', 'Create new spendmoneytxn.'),
-            (DELETE, 'SpendMoneyTxn/[uid]/', 'Delete selected spendmoneytxn.'),
-            (ALL, 'ReceiveMoneyTxn/', 'Return all receivemoneytxns for an AccountRight company file.'),
-            (GET, 'ReceiveMoneyTxn/[uid]/', 'Return selected receivemoneytxn.'),
-            (PUT, 'ReceiveMoneyTxn/[uid]/', 'Update selected receivemoneytxn.'),
-            (POST, 'ReceiveMoneyTxn/', 'Create new receivemoneytxn.'),
-            (DELETE, 'ReceiveMoneyTxn/[uid]/', 'Delete selected receivemoneytxn.'),
+            (ALL, '', 'banking type'),
+            (CRUD, 'SpendMoneyTxn/', 'spend money transaction'),
+            (CRUD, 'ReceiveMoneyTxn/', 'receive money transaction'),
         ],
     },
     'Contact/': {
-        'plural': 'contacts',
+        'name': 'contacts',
         'methods': [
-            (ALL, '', 'Return all contact types for an AccountRight company file.'),
-            (ALL, 'Customer/', 'Return all customer contacts for an AccountRight company file.'),
-            (GET, 'Customer/[uid]/', 'Return selected customer contact.'),
-            (PUT, 'Customer/[uid]/', 'Update selected customer contact.'),
-            (POST, 'Customer/', 'Create new customer contact.'),
-            (DELETE, 'Customer/[uid]/', 'Delete selected customer contact.'),
-            (ALL, 'Supplier/', 'Return all supplier contacts for an AccountRight company file.'),
-            (GET, 'Supplier/[uid]/', 'Return selected supplier contact.'),
-            (PUT, 'Supplier/[uid]/', 'Update selected supplier contact.'),
-            (POST, 'Supplier/', 'Create new supplier contact.'),
-            (DELETE, 'Supplier/[uid]/', 'Delete selected supplier contact.'),
+            (ALL, '', 'contact type'),
+            (CRUD, 'Customer/', 'customer contact'),
+            (CRUD, 'Supplier/', 'supplier contact'),
         ],
     },
     'Sale/Invoice/': {
-        'plural': 'invoices',
+        'name': 'invoices',
         'methods': [
-            (ALL, '', 'Return all sale invoice types for an AccountRight company file.'),
-            (ALL, 'Item/', 'Return item type sale invoices for an AccountRight company file.'),
-            (GET, 'Item/[uid]/', 'Return selected item type sale invoice.'),
-            (PUT, 'Item/[uid]/', 'Update selected item type sale invoice.'),
-            (POST, 'Item/', 'Create new item type sale invoice.'),
-            (DELETE, 'Item/[uid]/', 'Delete selected item type sale invoice.'),
-            (ALL, 'Service/', 'Return service type sale invoices for an AccountRight company file.'),
-            (GET, 'Service/[uid]/', 'Return selected service type sale invoice.'),
-            (PUT, 'Service/[uid]/', 'Update selected service type sale invoice.'),
-            (POST, 'Service/', 'Create new service type sale invoice.'),
-            (DELETE, 'Service/[uid]/', 'Delete selected service type sale invoice.'),
+            (ALL, '', 'sale invoice type'),
+            (CRUD, 'Item/', 'item type sale invoice'),
+            (CRUD, 'Service/', 'service type sale invoice'),
         ]
     },
     'GeneralLedger/': {
-        'plural': 'general_ledger',
+        'name': 'general_ledger',
         'methods': [
-            (ALL, 'TaxCode/', 'Return tax codes set up with an AccountRight company file.'),
-            (GET, 'TaxCode/[uid]/', 'Return selected tax code.'),
-            (PUT, 'TaxCode/[uid]/', 'Update selected tax codes.'),
-            (POST, 'TaxCode/', 'Create new tax code.'),
-            (DELETE, 'TaxCode/[uid]/', 'Delete selected tax code.'),
-            (ALL, 'Account/', 'Return accounts set up with an AccountRight company file.'),
-            (GET, 'Account/[uid]/', 'Return selected account.'),
-            (PUT, 'Account/[uid]/', 'Update selected accounts.'),
-            (POST, 'Account/', 'Create new account.'),
-            (DELETE, 'Account/[uid]/', 'Delete selected account.'),
-            (ALL, 'Category/', 'Return categories for cost center tracking.'),
-            (GET, 'Category/[uid]/', 'Return selected category.'),
-            (PUT, 'Category/[uid]/', 'Update selected categories.'),
-            (POST, 'Category/', 'Create new category.'),
-            (DELETE, 'Category/[uid]/', 'Delete selected category.'),
+            (CRUD, 'TaxCode/', 'tax code'),
+            (CRUD, 'Account/', 'account'),
+            (CRUD, 'Category/', 'cost center tracking category'),
         ]
     },
     'Inventory/': {
-        'plural': 'inventory',
+        'name': 'inventory',
         'methods': [
-            (ALL, 'Item/', 'Return inventory items for an AccountRight company file.'),
-            (GET, 'Item/[uid]/', 'Return selected inventory item.'),
-            (PUT, 'Item/[uid]/', 'Update selected inventory items.'),
-            (POST, 'Item/', 'Create new inventory item.'),
-            (DELETE, 'Item/[uid]/', 'Delete selected inventory item.'),
+            (CRUD, 'Item/', 'inventory item'),
         ]
     },
     'Purchase/Order/': {
-        'plural': 'purchase_orders',
+        'name': 'purchase_orders',
         'methods': [
-            (ALL, '', 'Return all purchase order types for an AccountRight company file.'),
-            (ALL, 'Item/', 'Return item type purchase orders for an AccountRight company file.'),
-            (GET, 'Item/[uid]/', 'Return selected item type purchase order.'),
-            (PUT, 'Item/[uid]/', 'Update selected item type purchase order.'),
-            (POST, 'Item/', 'Create new item type purchase order.'),
-            (DELETE, 'Item/[uid]/', 'Delete selected item type purchase order.'),
+            (ALL, '', 'purchase order type'),
+            (CRUD, 'Item/', 'item type purchase order'),
         ]
     },
     'Purchase/Bill/': {
-        'plural': 'purchase_bills',
+        'name': 'purchase_bills',
         'methods': [
-            (ALL, '', 'Return all purchase bill types for an AccountRight company file.'),
-            (ALL, 'Item/', 'Return item type purchase bills for an AccountRight company file.'),
-            (GET, 'Item/[uid]/', 'Return selected item type purchase bill.'),
-            (PUT, 'Item/[uid]/', 'Update selected item type purchase bill.'),
-            (POST, 'Item/', 'Create new item type purchase bill.'),
-            (DELETE, 'Item/[uid]/', 'Delete selected item type purchase bill.'),
-            (ALL, 'Service/', 'Return service type purchase bills for an AccountRight company file.'),
-            (GET, 'Service/[uid]/', 'Return selected service type purchase bill.'),
-            (PUT, 'Service/[uid]/', 'Update selected service type purchase bill.'),
-            (POST, 'Service/', 'Create new service type purchase bill.'),
-            (DELETE, 'Service/[uid]/', 'Delete selected service type purchase bill.'),
-            (ALL, 'Miscellaneous/', 'Return miscellaneous type purchase bills for an AccountRight company file.'),
-            (GET, 'Miscellaneous/[uid]/', 'Return selected miscellaneous type purchase bill.'),
-            (PUT, 'Miscellaneous/[uid]/', 'Update selected miscellaneous type purchase bill.'),
-            (POST, 'Miscellaneous/', 'Create new miscellaneous type purchase bill.'),
-            (DELETE, 'Miscellaneous/[uid]/', 'Delete selected miscellaneous type purchase bill.'),
+            (ALL, '', 'purchase bill type'),
+            (CRUD, 'Item/', 'item type purchase bill'),
+            (CRUD, 'Service/', 'service type purchase bill'),
+            (CRUD, 'Miscellaneous/', 'miscellaneous type purchase bill'),
         ]
     },
 }
+
+METHOD_MAPPING = {
+    ALL: {
+        'endpoint': lambda base: base,
+        'hint': lambda name: 'Return all %s for an AccountRight company file.' % pluralise(name)
+    },
+    GET: {
+        'endpoint': lambda base: base + '[uid]/',
+        'hint': lambda name: 'Return selected %s.' % name
+    },
+    PUT: {
+        'endpoint': lambda base: base + '[uid]/',
+        'hint': lambda name: 'Update selected %s.' % name
+    },
+    POST: {
+        'endpoint': lambda base: base,
+        'hint': lambda name: 'Create new %s.' % name
+    },
+    DELETE: {
+        'endpoint': lambda base: base + '[uid]/',
+        'hint': lambda name: 'Delete selected %s.' % name
+    },
+}
diff --git a/myob/managers.py b/myob/managers.py
index b8edc36..1644590 100755
--- a/myob/managers.py
+++ b/myob/managers.py
@@ -3,7 +3,7 @@ import requests
 from datetime import date
 
 from .constants import DEFAULT_PAGE_SIZE, MYOB_BASE_URL
-from .endpoints import METHOD_ORDER
+from .endpoints import CRUD, METHOD_MAPPING, METHOD_ORDER
 from .exceptions import (
     MyobBadRequest,
     MyobExceptionUnknown,
@@ -14,7 +14,7 @@ from .exceptions import (
 
 
 class Manager:
-    def __init__(self, name, credentials, company_id=None, endpoints=[]):
+    def __init__(self, name, credentials, company_id=None, endpoints=[], raw_endpoints=[]):
         self.credentials = credentials
         self.name = '_'.join(p for p in name.rstrip('/').split('/') if '[' not in p)
         self.base_url = MYOB_BASE_URL
@@ -26,9 +26,22 @@ class Manager:
         self.company_id = company_id
 
         # Build ORM methods from given url endpoints.
-        # Sort them first, to determine duplicate disambiguation order.
-        sorted_endpoints = sorted(endpoints, key=lambda x: METHOD_ORDER.index(x[0]))
-        for method, endpoint, hint in sorted_endpoints:
+        for method, base, name in endpoints:
+            if method == CRUD:
+                for m in METHOD_ORDER:
+                    self.build_method(
+                        m,
+                        METHOD_MAPPING[m]['endpoint'](base),
+                        METHOD_MAPPING[m]['hint'](name),
+                    )
+            else:
+                self.build_method(
+                    method,
+                    METHOD_MAPPING[method]['endpoint'](base),
+                    METHOD_MAPPING[method]['hint'](name),
+                )
+        # Build raw methods (ones where we don't want to tinker with the endpoint or hint)
+        for method, endpoint, hint in raw_endpoints:
             self.build_method(method, endpoint, hint)
 
     def build_method(self, method, endpoint, hint):
@@ -72,7 +85,7 @@ class Manager:
 
             if response.status_code == 200:
                 # We don't want to be deserialising binary responses..
-                if not response.headers.get('content-type', '').startswith('application/json'):
+                if not response.headers['content-type'].startswith('application/json'):
                     return response.content
 
                 return response.json()
diff --git a/myob/utils.py b/myob/utils.py
new file mode 100644
index 0000000..500580e
--- /dev/null
+++ b/myob/utils.py
@@ -0,0 +1,5 @@
+def pluralise(s):
+    if s.endswith('y'):
+        return s[:-1] + 'ies'
+    else:
+        return s + 's'


Test patch:
diff --git a/tests/endpoints.py b/tests/endpoints.py
index 660b8ff..6272b56 100644
--- a/tests/endpoints.py
+++ b/tests/endpoints.py
@@ -83,16 +83,16 @@ class EndpointTests(TestCase):
         self.assertEqual(repr(self.companyfile.banking), (
             "BankingManager:\n"
             "                             all() - Return all banking types for an AccountRight company file.\n"
-            "       delete_receivemoneytxn(uid) - Delete selected receivemoneytxn.\n"
-            "         delete_spendmoneytxn(uid) - Delete selected spendmoneytxn.\n"
-            "          get_receivemoneytxn(uid) - Return selected receivemoneytxn.\n"
-            "            get_spendmoneytxn(uid) - Return selected spendmoneytxn.\n"
-            "        post_receivemoneytxn(data) - Create new receivemoneytxn.\n"
-            "          post_spendmoneytxn(data) - Create new spendmoneytxn.\n"
-            "    put_receivemoneytxn(uid, data) - Update selected receivemoneytxn.\n"
-            "      put_spendmoneytxn(uid, data) - Update selected spendmoneytxn.\n"
-            "                 receivemoneytxn() - Return all receivemoneytxns for an AccountRight company file.\n"
-            "                   spendmoneytxn() - Return all spendmoneytxns for an AccountRight company file."
+            "       delete_receivemoneytxn(uid) - Delete selected receive money transaction.\n"
+            "         delete_spendmoneytxn(uid) - Delete selected spend money transaction.\n"
+            "          get_receivemoneytxn(uid) - Return selected receive money transaction.\n"
+            "            get_spendmoneytxn(uid) - Return selected spend money transaction.\n"
+            "        post_receivemoneytxn(data) - Create new receive money transaction.\n"
+            "          post_spendmoneytxn(data) - Create new spend money transaction.\n"
+            "    put_receivemoneytxn(uid, data) - Update selected receive money transaction.\n"
+            "      put_spendmoneytxn(uid, data) - Update selected spend money transaction.\n"
+            "                 receivemoneytxn() - Return all receive money transactions for an AccountRight company file.\n"
+            "                   spendmoneytxn() - Return all spend money transactions for an AccountRight company file."
         ))
         self.assertEndpointReached(self.companyfile.banking.all, {}, 'GET', f'/{CID}/Banking/')
         self.assertEndpointReached(self.companyfile.banking.spendmoneytxn, {}, 'GET', f'/{CID}/Banking/SpendMoneyTxn/')
@@ -141,12 +141,12 @@ class EndpointTests(TestCase):
             "       delete_service(uid) - Delete selected service type sale invoice.\n"
             "             get_item(uid) - Return selected item type sale invoice.\n"
             "          get_service(uid) - Return selected service type sale invoice.\n"
-            "                    item() - Return item type sale invoices for an AccountRight company file.\n"
+            "                    item() - Return all item type sale invoices for an AccountRight company file.\n"
             "           post_item(data) - Create new item type sale invoice.\n"
             "        post_service(data) - Create new service type sale invoice.\n"
             "       put_item(uid, data) - Update selected item type sale invoice.\n"
             "    put_service(uid, data) - Update selected service type sale invoice.\n"
-            "                 service() - Return service type sale invoices for an AccountRight company file."
+            "                 service() - Return all service type sale invoices for an AccountRight company file."
         ))
         self.assertEndpointReached(self.companyfile.invoices.all, {}, 'GET', f'/{CID}/Sale/Invoice/')
         self.assertEndpointReached(self.companyfile.invoices.item, {}, 'GET', f'/{CID}/Sale/Invoice/Item/')
@@ -163,21 +163,21 @@ class EndpointTests(TestCase):
     def test_general_ledger(self):
         self.assertEqual(repr(self.companyfile.general_ledger), (
             "GeneralLedgerManager:\n"
-            "                  account() - Return accounts set up with an AccountRight company file.\n"
-            "                 category() - Return categories for cost center tracking.\n"
+            "                  account() - Return all accounts for an AccountRight company file.\n"
+            "                 category() - Return all cost center tracking categories for an AccountRight company file.\n"
             "        delete_account(uid) - Delete selected account.\n"
-            "       delete_category(uid) - Delete selected category.\n"
+            "       delete_category(uid) - Delete selected cost center tracking category.\n"
             "        delete_taxcode(uid) - Delete selected tax code.\n"
             "           get_account(uid) - Return selected account.\n"
-            "          get_category(uid) - Return selected category.\n"
+            "          get_category(uid) - Return selected cost center tracking category.\n"
             "           get_taxcode(uid) - Return selected tax code.\n"
             "         post_account(data) - Create new account.\n"
-            "        post_category(data) - Create new category.\n"
+            "        post_category(data) - Create new cost center tracking category.\n"
             "         post_taxcode(data) - Create new tax code.\n"
-            "     put_account(uid, data) - Update selected accounts.\n"
-            "    put_category(uid, data) - Update selected categories.\n"
-            "     put_taxcode(uid, data) - Update selected tax codes.\n"
-            "                  taxcode() - Return tax codes set up with an AccountRight company file."
+            "     put_account(uid, data) - Update selected account.\n"
+            "    put_category(uid, data) - Update selected cost center tracking category.\n"
+            "     put_taxcode(uid, data) - Update selected tax code.\n"
+            "                  taxcode() - Return all tax codes for an AccountRight company file."
         ))
         self.assertEndpointReached(self.companyfile.general_ledger.taxcode, {}, 'GET', f'/{CID}/GeneralLedger/TaxCode/')
         self.assertEndpointReached(self.companyfile.general_ledger.get_taxcode, {'uid': UID}, 'GET', f'/{CID}/GeneralLedger/TaxCode/{UID}/')
@@ -200,9 +200,9 @@ class EndpointTests(TestCase):
             "InventoryManager:\n"
             "       delete_item(uid) - Delete selected inventory item.\n"
             "          get_item(uid) - Return selected inventory item.\n"
-            "                 item() - Return inventory items for an AccountRight company file.\n"
+            "                 item() - Return all inventory items for an AccountRight company file.\n"
             "        post_item(data) - Create new inventory item.\n"
-            "    put_item(uid, data) - Update selected inventory items."
+            "    put_item(uid, data) - Update selected inventory item."
         ))
         self.assertEndpointReached(self.companyfile.inventory.item, {}, 'GET', f'/{CID}/Inventory/Item/')
         self.assertEndpointReached(self.companyfile.inventory.get_item, {'uid': UID}, 'GET', f'/{CID}/Inventory/Item/{UID}/')
@@ -216,7 +216,7 @@ class EndpointTests(TestCase):
             "                  all() - Return all purchase order types for an AccountRight company file.\n"
             "       delete_item(uid) - Delete selected item type purchase order.\n"
             "          get_item(uid) - Return selected item type purchase order.\n"
-            "                 item() - Return item type purchase orders for an AccountRight company file.\n"
+            "                 item() - Return all item type purchase orders for an AccountRight company file.\n"
             "        post_item(data) - Create new item type purchase order.\n"
             "    put_item(uid, data) - Update selected item type purchase order."
         ))
@@ -237,15 +237,15 @@ class EndpointTests(TestCase):
             "                   get_item(uid) - Return selected item type purchase bill.\n"
             "          get_miscellaneous(uid) - Return selected miscellaneous type purchase bill.\n"
             "                get_service(uid) - Return selected service type purchase bill.\n"
-            "                          item() - Return item type purchase bills for an AccountRight company file.\n"
-            "                 miscellaneous() - Return miscellaneous type purchase bills for an AccountRight company file.\n"
+            "                          item() - Return all item type purchase bills for an AccountRight company file.\n"
+            "                 miscellaneous() - Return all miscellaneous type purchase bills for an AccountRight company file.\n"
             "                 post_item(data) - Create new item type purchase bill.\n"
             "        post_miscellaneous(data) - Create new miscellaneous type purchase bill.\n"
             "              post_service(data) - Create new service type purchase bill.\n"
             "             put_item(uid, data) - Update selected item type purchase bill.\n"
             "    put_miscellaneous(uid, data) - Update selected miscellaneous type purchase bill.\n"
             "          put_service(uid, data) - Update selected service type purchase bill.\n"
-            "                       service() - Return service type purchase bills for an AccountRight company file."
+            "                       service() - Return all service type purchase bills for an AccountRight company file."
         ))
         self.assertEndpointReached(self.companyfile.purchase_bills.all, {}, 'GET', f'/{CID}/Purchase/Bill/')
         self.assertEndpointReached(self.companyfile.purchase_bills.item, {}, 'GET', f'/{CID}/Purchase/Bill/Item/')