Skip to content

Commit 40405dc

Browse files
Merge pull request #25 from AASHE/chunk-get-orgs
Chunk get orgs
2 parents fb26e35 + 7acc8e1 commit 40405dc

9 files changed

Lines changed: 282 additions & 264 deletions

File tree

membersuite_api_client/memberships/services.py

Lines changed: 62 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@
66
"""
77
from zeep.exceptions import TransportError
88

9+
from ..mixins import ChunkQueryMixin
910
from .models import Membership, MembershipProduct
1011
from ..utils import convert_ms_object
1112
import datetime
1213

1314

14-
class MembershipService(object):
15+
class MembershipService(ChunkQueryMixin, object):
1516

1617
def __init__(self, client):
1718
"""
1819
Accepts a ConciergeClient to connect with MemberSuite
1920
"""
2021
self.client = client
2122

22-
def get_memberships_for_org(self, account_num):
23+
def get_memberships_for_org(self, account_num, verbose=False):
2324
"""
2425
Retrieve all memberships associated with an organization
2526
"""
@@ -28,18 +29,13 @@ def get_memberships_for_org(self, account_num):
2829

2930
query = "SELECT Objects() FROM Membership " \
3031
"WHERE Owner = '%s'" % account_num
31-
result = self.client.runSQL(query)
32-
msql_result = result["body"]["ExecuteMSQLResult"]
33-
obj_result = msql_result["ResultValue"]["ObjectSearchResult"]
34-
if obj_result['Objects']:
35-
objects = obj_result['Objects']['MemberSuiteObject']
36-
if not msql_result["Errors"] and len(objects):
37-
return self.package_memberships(objects)
38-
return None
39-
40-
def get_all_memberships(self, since_when=None, results=None,
41-
start_record=0, limit_to=200,
42-
depth=1, max_depth=None):
32+
33+
membership_list = self.get_long_query(query, verbose=verbose)
34+
return membership_list
35+
36+
def get_all_memberships(
37+
self, limit_to=100, max_calls=None, parameters=None,
38+
since_when=None, start_record=0, verbose=False):
4339
"""
4440
Retrieve all memberships updated since "since_when"
4541
@@ -51,100 +47,65 @@ def get_all_memberships(self, since_when=None, results=None,
5147
self.client.request_session()
5248

5349
query = "SELECT Objects() FROM Membership"
50+
51+
# collect all where parameters into a list of
52+
# (key, operator, value) tuples
53+
where_params = []
54+
55+
if parameters:
56+
for k, v in parameters.items():
57+
where_params.append((k, "=", v))
58+
5459
if since_when:
55-
query += " WHERE LastModifiedDate > '{since_when} 00:00:00'" \
56-
" ORDER BY LocalID".format(
57-
since_when=datetime.date.today() -
58-
datetime.timedelta(days=since_when))
59-
else:
60-
query += " ORDER BY LocalID"
61-
62-
try:
63-
result = self.client.runSQL(
64-
query=query,
65-
start_record=start_record,
66-
limit_to=limit_to,
67-
)
68-
69-
except TransportError:
70-
# API Intermittently fails and kicks a 504,
71-
# this is a way to retry if that happens.
72-
result = self.get_all_memberships(
73-
since_when=since_when,
74-
results=results,
75-
start_record=start_record,
76-
limit_to=limit_to,
77-
depth=depth,
78-
max_depth=max_depth,
79-
)
80-
81-
msql_result = result['body']["ExecuteMSQLResult"]
82-
if (not msql_result['Errors'] and msql_result["ResultValue"]
83-
["ObjectSearchResult"]["Objects"]):
84-
new_results = self.package_memberships(msql_result
85-
["ResultValue"]
86-
["ObjectSearchResult"]
87-
["Objects"]
88-
["MemberSuiteObject"]
89-
) + (results or [])
90-
91-
# Check if the queryset was completely full. If so, there may be
92-
# More results we need to query
93-
if len(new_results) >= limit_to and not depth == max_depth:
94-
new_results = self.get_all_memberships(
95-
since_when=since_when,
96-
results=new_results,
97-
start_record=start_record + limit_to,
98-
limit_to=limit_to,
99-
depth=depth+1,
100-
max_depth=max_depth
101-
)
102-
return new_results
103-
else:
104-
return results
105-
106-
def get_all_membership_products(self):
60+
d = datetime.date.today() - datetime.timedelta(days=since_when)
61+
where_params.append(
62+
('LastModifiedDate', ">", "'%s 00:00:00'" % d))
63+
64+
if where_params:
65+
query += " WHERE "
66+
query += " AND ".join(
67+
["%s %s %s" % (p[0], p[1], p[2]) for p in where_params])
68+
69+
query += " ORDER BY LocalID"
70+
71+
# note, get_long_query is overkill when just looking at
72+
# one org, but it still only executes once
73+
# `get_long_query` uses `ms_object_to_model` to return Organizations
74+
membership_list = self.get_long_query(
75+
query, limit_to=limit_to, max_calls=max_calls,
76+
start_record=start_record, verbose=verbose)
77+
78+
return membership_list
79+
80+
def ms_object_to_model(self, ms_obj):
81+
" Converts an individual result to a Subscription Model "
82+
sane_obj = convert_ms_object(
83+
ms_obj['Fields']['KeyValueOfstringanyType'])
84+
return Membership(sane_obj)
85+
86+
87+
class MembershipProductService(ChunkQueryMixin, object):
88+
89+
def __init__(self, client):
90+
"""
91+
Accepts a ConciergeClient to connect with MemberSuite
92+
"""
93+
self.client = client
94+
95+
def get_all_membership_products(self, verbose=False):
10796
"""
10897
Retrieves membership product objects
10998
"""
11099
if not self.client.session_id:
111100
self.client.request_session()
112101

113102
query = "SELECT Objects() FROM MembershipDuesProduct"
114-
result = self.client.runSQL(query)
115-
msql_result = result['body']['ExecuteMSQLResult']
116-
if not msql_result['Errors']:
117-
return self.package_membership_products(msql_result)
118-
else:
119-
return None
120-
121-
def package_memberships(self, object_list):
122-
"""
123-
Loops through MS objects returned from queries to turn them into
124-
Membership objects and pack them into a list for later use.
125-
"""
126-
membership_list = []
127-
for obj in object_list:
128-
if type(obj) != str:
129-
sane_obj = convert_ms_object(
130-
obj['Fields']['KeyValueOfstringanyType'])
131-
membership = Membership(sane_obj)
132-
membership_list.append(membership)
133103

134-
return membership_list
104+
membership_product_list = self.get_long_query(query, verbose=verbose)
105+
return membership_product_list
135106

136-
def package_membership_products(self, msql_result):
137-
"""
138-
Loops through MS objects returned from queries to turn them into
139-
MembershipProduct objects and pack them into a list for later use.
140-
"""
141-
obj_result = msql_result['ResultValue']['ObjectSearchResult']
142-
objects = obj_result['Objects']['MemberSuiteObject']
143-
product_list = []
144-
for obj in objects:
145-
sane_obj = convert_ms_object(
146-
obj['Fields']['KeyValueOfstringanyType']
147-
)
148-
product = MembershipProduct(sane_obj)
149-
product_list.append(product)
150-
return product_list
107+
def ms_object_to_model(self, ms_obj):
108+
" Converts an individual result to a Subscription Model "
109+
sane_obj = convert_ms_object(
110+
ms_obj['Fields']['KeyValueOfstringanyType'])
111+
return MembershipProduct(sane_obj)

membersuite_api_client/mixins.py

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
from retrying import retry
2+
import datetime
3+
4+
from .exceptions import ExecuteMSQLError
5+
6+
RETRY_ATTEMPTS = 10
27

38

49
class ChunkQueryMixin():
@@ -15,28 +20,41 @@ class ChunkQueryMixin():
1520
"""
1621

1722
def get_long_query(
18-
self, base_query, retry_attempts=2, limit_to=200, max_calls=None):
23+
self, base_query, limit_to=100, max_calls=None,
24+
start_record=0, verbose=False):
1925
"""
2026
Takes a base query for all objects and recursively requests them
2127
22-
@base_query - the base query to be executed
23-
@retry_attempts - the number of times to retry a query when it fails
24-
@limit_to - how many rows to query for in each chunk
25-
@max_recursion_depth - None is infinite
28+
:param str base_query: the base query to be executed
29+
:param int limit_to: how many rows to query for in each chunk
30+
:param int max_calls: the max calls(chunks to request) None is infinite
31+
:param int start_record: the first record to return from the query
32+
:param bool verbose: print progress to stdout
33+
:return: a list of Organization objects
2634
"""
2735

28-
@retry(stop_max_attempt_number=retry_attempts)
29-
def run_query(base_query, start_record, limit_to):
36+
@retry(stop_max_attempt_number=RETRY_ATTEMPTS, wait_fixed=2000)
37+
def run_query(base_query, start_record, limit_to, verbose):
3038
# inline method to take advantage of retry
39+
40+
if verbose:
41+
print("[start: %d limit: %d]" % (start_record, limit_to))
42+
start = datetime.datetime.now()
3143
result = self.client.runSQL(
3244
query=base_query,
3345
start_record=start_record,
3446
limit_to=limit_to,
3547
)
48+
end = datetime.datetime.now()
49+
if verbose:
50+
print("[%s - %s]" % (start, end))
3651
return self.result_to_models(result)
3752

38-
record_index = 0
39-
result = run_query(base_query, record_index, limit_to)
53+
if verbose:
54+
print(base_query)
55+
56+
record_index = start_record
57+
result = run_query(base_query, record_index, limit_to, verbose)
4058
all_objects = result
4159
call_count = 1
4260
"""
@@ -49,7 +67,32 @@ def run_query(base_query, start_record, limit_to):
4967
len(result) >= limit_to):
5068

5169
record_index += len(result) # should be `limit_to`
52-
all_objects += run_query(base_query, record_index, limit_to)
70+
result = run_query(
71+
base_query, record_index, limit_to, verbose)
72+
all_objects += result
5373
call_count += 1
5474

5575
return all_objects
76+
77+
def result_to_models(self, result):
78+
"""
79+
this is the 'transorm' part of ETL:
80+
converts the result of the SQL to Models
81+
"""
82+
mysql_result = result['body']['ExecuteMSQLResult']
83+
84+
if not mysql_result['Errors']:
85+
obj_result = mysql_result['ResultValue']['ObjectSearchResult']
86+
if not obj_result['Objects']:
87+
return []
88+
objects = obj_result['Objects']['MemberSuiteObject']
89+
90+
model_list = []
91+
for obj in objects:
92+
model = self.ms_object_to_model(obj)
93+
model_list.append(model)
94+
95+
return model_list
96+
97+
else:
98+
raise ExecuteMSQLError(result)

0 commit comments

Comments
 (0)