diff --git a/README.md b/README.md index 0d37b30..cafed76 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,190 @@ Type: `Boolean` `LOG_BODY` is default to true, set to false to remove logging request and response body to Moesif. +## Update User + +### Update A Single User +Create or update a user profile in Moesif. +The metadata field can be any customer demographic or other info you want to store. +Only the `user_id` field is required. +For details, visit the [Python API Reference](https://www.moesif.com/docs/api?python#update-a-user). + +```python +from moesif_aws_lambda.middleware import * + +moesif_options = { + 'LOG_BODY': True, + 'DEBUG': True, +} + +# Only user_id is required. +# Campaign object is optional, but useful if you want to track ROI of acquisition channels +# See https://www.moesif.com/docs/api#users for campaign schema +# metadata can be any custom object +user = { + 'user_id': '12345', + 'company_id': '67890', # If set, associate user with a company object + 'campaign': { + 'utm_source': 'google', + 'utm_medium': 'cpc', + 'utm_campaign': 'adwords', + 'utm_term': 'api+tooling', + 'utm_content': 'landing' + }, + 'metadata': { + 'email': 'john@acmeinc.com', + 'first_name': 'John', + 'last_name': 'Doe', + 'title': 'Software Engineer', + 'sales_info': { + 'stage': 'Customer', + 'lifetime_value': 24000, + 'account_owner': 'mary@contoso.com' + }, + } +} + +update_user(user, moesif_options) +``` + +### Update Users in Batch +Similar to update_user, but used to update a list of users in one batch. +Only the `user_id` field is required. +For details, visit the [Python API Reference](https://www.moesif.com/docs/api?python#update-users-in-batch). + +```python +from moesif_aws_lambda.middleware import * + +moesif_options = { + 'LOG_BODY': True, + 'DEBUG': True, +} + +userA = { + 'user_id': '12345', + 'company_id': '67890', # If set, associate user with a company object + 'metadata': { + 'email': 'john@acmeinc.com', + 'first_name': 'John', + 'last_name': 'Doe', + 'title': 'Software Engineer', + 'sales_info': { + 'stage': 'Customer', + 'lifetime_value': 24000, + 'account_owner': 'mary@contoso.com' + }, + } +} + +userB = { + 'user_id': '54321', + 'company_id': '67890', # If set, associate user with a company object + 'metadata': { + 'email': 'mary@acmeinc.com', + 'first_name': 'Mary', + 'last_name': 'Jane', + 'title': 'Software Engineer', + 'sales_info': { + 'stage': 'Customer', + 'lifetime_value': 48000, + 'account_owner': 'mary@contoso.com' + }, + } +} +update_users_batch([userA, userB], moesif_options) +``` + +## Update Company + +### Update A Single Company +Create or update a company profile in Moesif. +The metadata field can be any company demographic or other info you want to store. +Only the `company_id` field is required. +For details, visit the [Python API Reference](https://www.moesif.com/docs/api?python#update-a-company). + +```python +from moesif_aws_lambda.middleware import * + +moesif_options = { + 'LOG_BODY': True, + 'DEBUG': True, +} + +# Only company_id is required. +# Campaign object is optional, but useful if you want to track ROI of acquisition channels +# See https://www.moesif.com/docs/api#update-a-company for campaign schema +# metadata can be any custom object +company = { + 'company_id': '67890', + 'company_domain': 'acmeinc.com', # If domain is set, Moesif will enrich your profiles with publicly available info + 'campaign': { + 'utm_source': 'google', + 'utm_medium': 'cpc', + 'utm_campaign': 'adwords', + 'utm_term': 'api+tooling', + 'utm_content': 'landing' + }, + 'metadata': { + 'org_name': 'Acme, Inc', + 'plan_name': 'Free', + 'deal_stage': 'Lead', + 'mrr': 24000, + 'demographics': { + 'alexa_ranking': 500000, + 'employee_count': 47 + }, + } +} + +update_company(company, moesif_options) +``` + +### Update Companies in Batch +Similar to update_company, but used to update a list of companies in one batch. +Only the `company_id` field is required. +For details, visit the [Python API Reference](https://www.moesif.com/docs/api?python#update-companies-in-batch). + +```python +from moesif_aws_lambda.middleware import * + +moesif_options = { + 'LOG_BODY': True, + 'DEBUG': True, +} + +companyA = { + 'company_id': '67890', + 'company_domain': 'acmeinc.com', # If domain is set, Moesif will enrich your profiles with publicly available info + 'metadata': { + 'org_name': 'Acme, Inc', + 'plan_name': 'Free', + 'deal_stage': 'Lead', + 'mrr': 24000, + 'demographics': { + 'alexa_ranking': 500000, + 'employee_count': 47 + }, + } +} + +companyB = { + 'company_id': '09876', + 'company_domain': 'contoso.com', # If domain is set, Moesif will enrich your profiles with publicly available info + 'metadata': { + 'org_name': 'Contoso, Inc', + 'plan_name': 'Free', + 'deal_stage': 'Lead', + 'mrr': 48000, + 'demographics': { + 'alexa_ranking': 500000, + 'employee_count': 53 + }, + } +} + +update_companies_batch([companyA, companyB], moesif_options) +``` + ## Examples - [A complete example is available on GitHub](https://github.com/Moesif/moesif-aws-lambda-python-example). diff --git a/moesif_aws_lambda/middleware.py b/moesif_aws_lambda/middleware.py index 1104558..17eee54 100644 --- a/moesif_aws_lambda/middleware.py +++ b/moesif_aws_lambda/middleware.py @@ -5,6 +5,8 @@ from moesifapi.exceptions.api_exception import * from moesifapi.models import * from .client_ip import ClientIp +from .update_companies import Company +from .update_users import User from datetime import * import base64 import json @@ -16,6 +18,25 @@ except ImportError: from urllib.parse import urlencode +# Initialized the client +if os.environ["MOESIF_APPLICATION_ID"]: + api_client = MoesifAPIClient(os.environ["MOESIF_APPLICATION_ID"]).api +else: + raise Exception('Moesif Application ID is required in settings') + +def update_user(user_profile, moesif_options): + User().update_user(user_profile, api_client, moesif_options) + +def update_users_batch(user_profiles, moesif_options): + User().update_users_batch(user_profiles, api_client, moesif_options) + +def update_company(company_profile, moesif_options): + Company().update_company(company_profile, api_client, moesif_options) + +def update_companies_batch(companies_profiles, moesif_options): + Company().update_companies_batch(companies_profiles, api_client, moesif_options) + + def MoesifLogger(moesif_options): class log_data(LambdaDecorator): def __init__(self, handler): @@ -31,7 +52,6 @@ def __init__(self, handler): self.DEBUG = self.moesif_options.get('DEBUG', False) self.event = None self.context = None - self.start_time = datetime.utcnow() # Intialized the client if os.environ.get("MOESIF_APPLICATION_ID"): @@ -39,6 +59,16 @@ def __init__(self, handler): else: raise Exception('Moesif Application ID is required in settings') + def clear_state(self): + """Function to clear state of local variable""" + self.event = None + self.context = None + self.event_req = None + self.metadata = None + self.session_token = None + self.user_id = None + self.company_id = None + def get_user_id(self, event, context): """Function to fetch UserId""" username = None @@ -109,10 +139,19 @@ def process_body(self, body_wrapper): def before(self, event, context): """This function runs before the handler is invoked, is passed the event & context and must return an event & context too.""" + # Clear the state of the local variables + self.clear_state() + + # Set/Save event and context for use Skip Event function + self.event = event + self.context = context + # Request Method request_verb = event.get('httpMethod') if request_verb is None: print('MOESIF: [before] AWS Lambda trigger must be a Load Balancer or API Gateway See https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html or https://docs.aws.amazon.com/lambda/latest/dg/with-on-demand-https.html.') + self.event = None + self.context = None return event, context # Request headers @@ -128,9 +167,9 @@ def before(self, event, context): # Request Time epoch = event and event.get('request_context', {}).get('requestTimeEpoch') if epoch is not None: - request_time =datetime.utcfromtimestamp(epoch) + request_time = datetime.utcfromtimestamp(epoch) else: - request_time = self.start_time + request_time = datetime.utcnow() # Request Body req_body, req_transfer_encoding = self.process_body(event) @@ -213,66 +252,64 @@ def before(self, event, context): body = req_body, transfer_encoding = req_transfer_encoding) - # Set/Save event and context for use Skip Event function - self.event = event - self.context = context - # Return event, context return event, context def after(self, retval): """This function runs after the handler is invoked, is passed the response and must return an response too.""" - # Response body - resp_body, resp_transfer_encoding = self.process_body(retval) + + if self.event is not None: + # Response body + resp_body, resp_transfer_encoding = self.process_body(retval) - # Event Response object - event_rsp = EventResponseModel(time = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3], - status = retval.get('statusCode', 599), - headers = retval.get('headers', {}), - body = resp_body, - transfer_encoding = resp_transfer_encoding) + # Event Response object + event_rsp = EventResponseModel(time = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3], + status = retval.get('statusCode', 599), + headers = retval.get('headers', {}), + body = resp_body, + transfer_encoding = resp_transfer_encoding) - # Event object - event_model = EventModel(request = self.event_req, - response = event_rsp, - user_id = self.user_id, - company_id = self.company_id, - session_token = self.session_token, - metadata = self.metadata) - - # Mask Event Model - try: - mask_event_model = self.moesif_options.get('MASK_EVENT_MODEL', None) - if mask_event_model is not None: - event_model = mask_event_model(event_model) - except: - if self.DEBUG: - print("MOESIF Can not execute MASK_EVENT_MODEL function. Please check moesif settings.") + # Event object + event_model = EventModel(request = self.event_req, + response = event_rsp, + user_id = self.user_id, + company_id = self.company_id, + session_token = self.session_token, + metadata = self.metadata) + + # Mask Event Model + try: + mask_event_model = self.moesif_options.get('MASK_EVENT_MODEL', None) + if mask_event_model is not None: + event_model = mask_event_model(event_model) + except: + if self.DEBUG: + print("MOESIF Can not execute MASK_EVENT_MODEL function. Please check moesif settings.") - # Skip Event - try: - skip_event = self.moesif_options.get('SKIP', None) - if skip_event is not None: - if skip_event(self.event, self.context): - if self.DEBUG: - print('MOESIF Skip sending event to Moesif') - return retval - except: - if self.DEBUG: - print("MOESIF Having difficulty executing skip_event function. Please check moesif settings.") + # Skip Event + try: + skip_event = self.moesif_options.get('SKIP', None) + if skip_event is not None: + if skip_event(self.event, self.context): + if self.DEBUG: + print('MOESIF Skip sending event to Moesif') + return retval + except: + if self.DEBUG: + print("MOESIF Having difficulty executing skip_event function. Please check moesif settings.") - # Add direction field - event_model.direction = "Incoming" + # Add direction field + event_model.direction = "Incoming" + + # Send event to Moesif + if self.DEBUG: + print('Moesif Event Model:') + print(json.dumps(self.event)) + + event_send = self.api_client.create_event(event_model) + if self.DEBUG: + print('MOESIF ' + str(event_send)) - # Send event to Moesif - if self.DEBUG: - print('Moesif Event Model:') - pprint(self.event) - - event_send = self.api_client.create_event(event_model) - if self.DEBUG: - print('MOESIF ' + str(event_send)) - # Send response return retval diff --git a/moesif_aws_lambda/update_companies.py b/moesif_aws_lambda/update_companies.py new file mode 100644 index 0000000..873978b --- /dev/null +++ b/moesif_aws_lambda/update_companies.py @@ -0,0 +1,117 @@ +from moesifapi.moesif_api_client import * +from moesifapi.api_helper import * +from moesifapi.exceptions.api_exception import * +from moesifapi.models import * + + +class Company: + + + def update_company(self, company_profile, api_client, moesif_options): + DEBUG = moesif_options.get('DEBUG', False) + if not company_profile: + print('Expecting the input to be either of the type - CompanyModel, dict or json while updating company') + else: + if isinstance(company_profile, dict): + if 'company_id' in company_profile: + try: + api_client.update_company(CompanyModel.from_dictionary(company_profile)) + if DEBUG: + print('Company Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating company, with status code:") + print(inst.response_code) + else: + print('To update an company, an company_id field is required') + + elif isinstance(company_profile, CompanyModel): + if company_profile.company_id is not None: + try: + api_client.update_company(company_profile) + if DEBUG: + print('Company Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating company, with status code:") + print(inst.response_code) + else: + print('To update a company, a company_id field is required') + else: + try: + company_profile_json = APIHelper.json_deserialize(company_profile) + if 'company_id' in company_profile_json: + try: + api_client.update_company(CompanyModel.from_dictionary(company_profile_json)) + if DEBUG: + print('Company Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating company, with status code:") + print(inst.response_code) + else: + print('To update a company, an company_id field is required') + except: + print('Error while deserializing the json, please make sure the json is valid') + + def update_companies_batch(self, company_profiles, api_client, moesif_options): + DEBUG = moesif_options.get('DEBUG', False) + if not company_profiles: + print('Expecting the input to be either of the type - List of CompanyModel, dict or json while updating companies') + else: + if all(isinstance(company, dict) for company in company_profiles): + if all('company_id' in company for company in company_profiles): + try: + batch_profiles = [CompanyModel.from_dictionary(d) for d in company_profiles] + api_client.update_companies_batch(batch_profiles) + if DEBUG: + print('Company Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating companies, with status code:") + print(inst.response_code) + else: + print('To update companies, an company_id field is required') + + elif all(isinstance(company, CompanyModel) for company in company_profiles): + if all(company.company_id is not None for company in company_profiles): + try: + api_client.update_companies_batch(company_profiles) + if DEBUG: + print('Company Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating companies, with status code:") + print(inst.response_code) + else: + print('To update companies, an company_id field is required') + else: + try: + company_profiles_json = [APIHelper.json_deserialize(d) for d in company_profiles] + if all(isinstance(company, dict) for company in company_profiles_json) and all( + 'company_id' in user for user in company_profiles_json): + try: + batch_profiles = [CompanyModel.from_dictionary(d) for d in company_profiles_json] + api_client.update_companies_batch(batch_profiles) + if DEBUG: + print('Company Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating companies, with status code:") + print(inst.response_code) + else: + print('To update companies, an company_id field is required') + except: + print('Error while deserializing the json, please make sure the json is valid') diff --git a/moesif_aws_lambda/update_users.py b/moesif_aws_lambda/update_users.py new file mode 100644 index 0000000..8a6f803 --- /dev/null +++ b/moesif_aws_lambda/update_users.py @@ -0,0 +1,117 @@ +from moesifapi.models import * +from moesifapi.exceptions.api_exception import * +from moesifapi.api_helper import * +from moesifapi.moesif_api_client import * + + +class User: + + + def update_user(self, user_profile, api_client, moesif_options): + DEBUG = moesif_options.get('DEBUG', False) + if not user_profile: + print('Expecting the input to be either of the type - UserModel, dict or json while updating user') + else: + if isinstance(user_profile, dict): + if 'user_id' in user_profile: + try: + api_client.update_user(UserModel.from_dictionary(user_profile)) + if DEBUG: + print('User Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating user, with status code:") + print(inst.response_code) + else: + print('To update an user, an user_id field is required') + + elif isinstance(user_profile, UserModel): + if user_profile.user_id is not None: + try: + api_client.update_user(user_profile) + if DEBUG: + print('User Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating user, with status code:") + print(inst.response_code) + else: + print('To update an user, an user_id field is required') + else: + try: + user_profile_json = APIHelper.json_deserialize(user_profile) + if 'user_id' in user_profile_json: + try: + api_client.update_user(UserModel.from_dictionary(user_profile_json)) + if DEBUG: + print('User Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating user, with status code:") + print(inst.response_code) + else: + print('To update an user, an user_id field is required') + except: + print('Error while deserializing the json, please make sure the json is valid') + + def update_users_batch(self, user_profiles, api_client, moesif_options): + DEBUG = moesif_options.get('DEBUG', False) + if not user_profiles: + print('Expecting the input to be either of the type - List of UserModel, dict or json while updating users') + else: + if all(isinstance(user, dict) for user in user_profiles): + if all('user_id' in user for user in user_profiles): + try: + batch_profiles = [UserModel.from_dictionary(d) for d in user_profiles] + api_client.update_users_batch(batch_profiles) + if DEBUG: + print('User Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating users, with status code:") + print(inst.response_code) + else: + print('To update users, an user_id field is required') + + elif all(isinstance(user, UserModel) for user in user_profiles): + if all(user.user_id is not None for user in user_profiles): + try: + api_client.update_users_batch(user_profiles) + if DEBUG: + print('User Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating users, with status code:") + print(inst.response_code) + else: + print('To update users, an user_id field is required') + else: + try: + user_profiles_json = [APIHelper.json_deserialize(d) for d in user_profiles] + if all(isinstance(user, dict) for user in user_profiles_json) and all( + 'user_id' in user for user in user_profiles_json): + try: + batch_profiles = [UserModel.from_dictionary(d) for d in user_profiles_json] + api_client.update_users_batch(batch_profiles) + if DEBUG: + print('User Profile updated successfully') + except APIException as inst: + if 401 <= inst.response_code <= 403: + print("Unauthorized access sending event to Moesif. Please check your Appplication Id.") + if DEBUG: + print("Error while updating users, with status code:") + print(inst.response_code) + else: + print('To update users, an user_id field is required') + except: + print('Error while deserializing the json, please make sure the json is valid') diff --git a/setup.py b/setup.py index 5e001a4..b4f061f 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='1.0.4', + version='1.0.5', description='Moesif Middleware to automatically log API calls from AWS Lambda functions', long_description=long_description,