Package: promoted-python-delivery-client
promoted-python-delivery-clientPython client SDK for the Promoted.ai Delivery APIFeaturesDemonstrates and implements the recommended practices and data types for calling Promoted.ai's Delivery API.Client-side position assignment and paging when not using results from Delivery API, for example when logging only or as part of an experiment control.Creating a PromotedDeliveryClientWe recommend creating aPromotedDeliveryClientin a separate file so it can be reused.
It is thread-safe and intended to be used as a singleton, leveraging the well-known Pythonrequestslibrary for calling Promoted.ai's services.PromotedClient.javaclient=PromotedDeliveryClient(delivery_endpoint=delivery_endpoint,delivery_api_key=delivery_api_key,delivery_timeout_millis=250,metrics_endpoint=metrics_endpoint,metrics_api_key=metrics_api_key,metrics_timeout_millis=1000)Client Configuration ParametersNameTypeDescriptiondelivery_endpointstrAPI endpoint for Promoted.ai's Delivery APIdelivery_api_keystrAPI key used in thex-api-keyheader for Promoted.ai's Delivery APIdelivery_timeout_millisintTimeout on the Delivery API call. Defaults to 250.metrics_endpointstrAPI endpoint for Promoted.ai's Metrics APImetrics_api_keystrAPI key used in thex-api-keyheader for Promoted.ai's Metrics APImetrics_timeout_millisintTimeout on the Metrics API call. Defaults to 3000.warmupboolOption to warm up the HTTP connection pool at initialization, defaults to False.thread_pool_sizeintNumber of threads to use in aThreadPoolExecutorto make background calls for metrics and shadow traffic, defaults to 5.apply_treatment_checkerfunc[[CohortMembership], bool]Optional function called during delivery, accepts an experiment and returns a boolean indicating whether the request should be considered part of the control group (False) or in the treatment arm of an experiment (True). If not set, the default behavior of checking the experiementarmis applied.max_request_insertionsintMaximum number of request insertions that will be passed to (and returned from) Delivery API. Defaults to 1000.shadow_traffic_delivery_ratefloat between 0 and 1rate = [0,1] of traffic that gets directed to Delivery API as "shadow traffic". Only applies to cases where Delivery API is not called. Defaults to 0 (no shadow traffic).blocking_shadow_trafficboolOption to make shadow traffic a blocking (as opposed to background) call to delivery API, defaults to False.perform_checksboolPerforms some validation that request fields are filled properly. These checks take time so this should be turned off once a request is satisfactory.Data TypesUserInfoBasic information about the request user.Field NameTypeOptional?Descriptionuser_idstrYesThe platform user id, cleared from Promoted logs.anon_user_idstrYesA different user id (presumably a UUID) disconnected from the platform user id (e.g. an "anonymous user id"), good for working with unauthenticated users or implementing right-to-be-forgotten.is_internal_userboolYesIf this user is a test user or not, defaults to false.CohortMembershipUseful fields for experimentation during the delivery phase.Field NameTypeOptional?DescriptionarmstrYes'CONTROL' or one of the TREATMENT values ('TREATMENT', 'TREATMENT1', etc.).cohort_idstrYesName of the cohort (e.g. "LOCAL_HOLDOUT" etc.)PropertiesProperties bag. Can create using aDict[str, object]. Has the JSON structure:"struct":{"product":{"id":"product3","title":"Product 3","url":"www.mymarket.com/p/3"// other key-value pairs...}}InsertionContent being served at a certain position.Field NameTypeOptional?Descriptionuser_infoUserInfoYesThe user info structure.insertion_idstrYesGenerated by the SDK (do not set)content_idstrNoIdentifier for the content to be ranked, must be set.retrieval_rankintYesOptional original ranking of this content item.retrieval_scorefloatYesOptional original quality score of this content item.propertiesPropertiesYesAny additional custom properties to associate. For v1 integrations, it is fine not to fill in all the properties.Insertions can be specified in a more compact manner using the request-levelinsertion_matrix_headersandinsertion_matrixfields. This can improve latency when there are many insertions or many properties.For example, instead of defining a request like so:insertion=[Insertion(content_id="28835",properties=Properties(struct={"price":1.23})),Insertion(content_id="37796",properties=Properties(struct={"price":0})),Insertion(content_id="49815"),]req=Request(insertion=insertion,...)It could be defined as:insertion_matrix_headers=["contentId","price"]insertion_matrix=[["28835",1.23],["37796",0],["49815",None],]req=Request(insertion_matrix_headers=insertion_matrix_headers,insertion_matrix=insertion_matrix,...)Things to note:Properties with nestedstructs should concatenate paths with a.(period).Properties which don't exist for an insertion must be specified asNone.Theperform_checksclient parameter can help ensure correct usage.SizeUser's screen dimensions.Field NameTypeOptional?DescriptionwidthintNoScreen widthheightintNoScreen heightScreenState of the screen including scaling.Field NameTypeOptional?DescriptionsizeSizeYesScreen sizescalefloatYesCurrent screen scaling factorClientHintsAlternative to user-agent strings. Seehttps://raw.githubusercontent.com/snowplow/iglu-central/master/schemas/org.ietf/http_client_hints/jsonschema/1-0-0Field NameTypeOptional?Descriptionis_mobileboolYesMobile flagbrandClientBrandHint[]YesarchitecturestrYesmodelstrYesplatformstrYesplatform_versionstrYesua_full_versionstrYesClientBrandHintSeehttps://raw.githubusercontent.com/snowplow/iglu-central/master/schemas/org.ietf/http_client_hints/jsonschema/1-0-0Field NameTypeOptional?DescriptionbrandstrYesMobile flagversionstrYesLocationInformation about the user's location.Field NameTypeOptional?DescriptionlatitudefloatNoLocation latitudelongitudefloatNoLocation longitudeaccuracy_in_metersintYesLocation accuracy if availableBrowserInformation about the user's browser.Field NameTypeOptional?Descriptionuser_agentstrYesBrowser user agent stringviewport_sizeSizeYesSize of the browser viewportclient_hintsClientHintsYesHTTP client hints structureDeviceInformation about the user's device.Field NameTypeOptional?Descriptiondevice_typeone of (UNKNOWN_DEVICE_TYPE,DESKTOP,MOBILE,TABLET)YesType of devicebrandstrYes"Apple, "google", Samsung", etc.manufacturerstrYes"Apple", "HTC", Motorola", "HUAWEI", etc.identifierstrYesAndroid: android.os.Build.MODEL; iOS: iPhoneXX,YY, etc.screenScreenYesScreen dimensionsip_addressstrYesOriginating IP addresslocationLocationYesLocation informationbrowserBrowserYesBrowser informationPagingDescribes a page of insertionsField NameTypeOptional?DescriptionsizeintYesSize of the page being requestedoffsetintYesPage offsetRequestA request for content insertions.Field NameTypeOptional?Descriptionuser_infoUserInfoYesThe user info structure.requestIdstrYesGenerated by the SDK when needed (do not set)use_casestrYesOne of the use case enum values or strings, i.e. 'FEED', 'SEARCH', etc.propertiesPropertiesYesAny additional custom properties to associate.pagingPagingYesPaging parametersdeviceDeviceYesDevice information (as available)disable_personalizationboolYesIf true, disables personalized inputs into Delivery algorithm.DeliveryRequestInput todeliver, returns ranked insertions for display.Field NameTypeOptional?DescriptionrequestRequestNoThe underlying request for content, including all candidate insertions with content ids.experimentCohortMembershipYesA cohort to evaluation in experimentation.only_logboolYesDefaults to false. Set to true to log the request as the CONTROL arm of an experiment, not call Delivery API, but rather deliver paged insertions from the request.insertion_startintYesStart index in the request insertions in the list of ALL insertions. SeePages of Request Insertionsfor more details.DeliveryResponseOutput ofdeliver, includes the ranked insertions for you to display.Field NameTypeOptional?DescriptionresponseResponseNoThe reponse from Delivery API, which includes the insertions. These are from Delivery API (whendeliverwas called, i.e. we weren't either only-log or part of an experiment) or the input insertions (when the other conditions don't hold).client_request_idstrYesClient-generated request id sent to Delivery API and may be useful for logging and debugging. You may fill this in yourself if you have a suitable id, otherwise the SDK will generate one.execution_serverone of 'API' or 'SDK'YesIndicates if response insertions on a delivery request came from the API or the SDK.PromotedDeliveryClientMethodInputOutputDescriptiondeliverDeliveryRequestDeliveryResponseMakes a request (subject to experimentation) to Delivery API for insertions, which are then returned.Calling the Delivery APILet's say the previous code looks like this:defget_products(req:ProductRequest):products=...;//LogictogetproductsfromDB,applyfiltering,etc.sendSuccessToClient(products)We might modify to something like this:defget_products(req:ProductRequest):products=...;//LogictogetproductsfromDB,applyfiltering,etc.insertion:List[Insertion]=[]# Keep a map for reorderingproduct_map:Dict[str,Product]={}forproductinrange(products):ins=Insertion(content_id=product.product_id)insertion.append(ins)product_map[product.product_id]=productreq=Request(insertion=insertion,user_info=UserInfo(anon_user_id="abc"),paging=Paging(size=100,offset=0))delivery_req=DeliveryRequest(request=req)resp=client.deliver(delivery_req)ranked_products:List[Product]=[]forinsinrange(resp.response.insertion):ranked_products.append(product_map[ins.content_id]))sendSuccessToClient(ranked_products)Pages of Request InsertionsClients can send a subset of all request insertions to Promoted in Delivery API'srequest.insertionarray. Theinsertion_startproperty specifies the start index of the arrayrequest.insertionin the list of ALL request insertions.request.paging.offsetshould be set to the zero-based position in ALL request insertions (notthe relative position in therequest.insertionarray).ExamplesIf there are 10 items and all 10 items are inrequest.insertion, then insertion_start=0.If there are 10,000 items and the first 500 items are onrequest.insertion, then insertionStart=0.If there are 10,000 items and we want to send items [500,1000) onrequest.insertion, then insertionStart=500.If there are 10,000 items and we want to send the last page [9500,10000) onrequest.insertion, then insertionStart=9500.insertion_startis required to be less thanpaging.offsetor else aValueErrorwill result.Additional details:https://docs.promoted.ai/docs/ranking-requests#sending-even-more-request-insertionsLogging onlyYou can usedeliverbut add aonly_log: Trueparameter to theDeliveryRequest.PositionDo not set the insertionpositionfield in client code. The SDK and Delivery API will set it whendeliveris called.ExperimentsPromoted supports the ability to run Promoted-side experiments. Sometimes it is useful to run an experiment in your wherepromoted-java-delivery-clientis integrated (e.g. you want arm assignments to match your own internal experiment arm assignments).# Create a small config indicating the experiment is a 50-50 experiment where 10% of the users are activated.experiment_config=create_50_50_two_arm_experiment_config("promoted-v1",5,5)defget_products(req:ProductRequest):products=...;//LogictogetproductsfromDB,applyfiltering,etc.# This gets the anonymous user id from the request.anon_user_id=get_anon_user_id(req)experiment_membership=experimentConfig.check_membership(anon_user_id)req=Request(insertion=insertion,user_info=UserInfo(anon_user_id="abc"),paging=Paging(size=100,offset=0))# If experimentActivated can be false (e.g. only 5% of users get put into an experiment) and# you want the non-activated behavior to not call Delivery API, then you need to specify onlyLog to false.# This is common during ramp up.  `onlyLog` can be dropped if it's always false.## Example:# `onlyLog: experimentMembership is None`deliveryRequest=DeliveryRequest(request=req,experiment=experiment_membership)response=client.deliver(delivery_request)# ...Here's an example using custom arm assignment logic (not usingtwoArmExperimentConfig5050).# If you already use an experiment framework, it'll have the ability to return# (1) if a user is activated into an experiment and# (2) which arm to perform.#experimentMembership:CohortMembership=Noneifis_user_activated(experimentName,anon_user_id):in_treatment=is_user_in_treatment_arm(experiment_name,anon_user_id)# Only log if the user is activated into the experiment.experiment_membership=CohortMembership(cohort_id=experiment_name,arm=(CohortArm.TREATMENTifin_treatmentelseCohortArm.CONTROL);SDK DevelopmentPrereqswheelsetuptoolstwinebump2versionDevelopmentFollow the setup script from the GitHub Action.Build wheel:python setup.py bdist_wheelInstall locally:pip install dist/promoted_python_delivery_client-2.1.0-py3-none-any.whl --force-reinstallTry it out:See thescripts/directory.Create ascripts/.envfile with a few variables:DELIVERY_ENDPOINT=<GETMEFROMPROMOTED>DELIVERY_API_KEY=<GETMEFROMPROMOTED>METRICS_ENDPOINT=<GETMEFROMPROMOTED>METRICS_API_KEY=<GETMEFROMPROMOTED>Invoke (for example)python3 scripts/call_delivery.py.TestingUnit testsUse pytest:pytest tests/ReleaseCreate a development branch.  The following command will create a commit with the version update.bump2version [major|minor|patch].  These are the strings (e.g.major).  This is not the version number.Send a pull request and merge.Clean out thedist/directory and build the wheel.python3 -m twine upload dist/*PyPiYou should use an account with MFA setup.  Follow the API token UI for the username and password.FUTURE: Do this with a Github Action.Dependenciesdataclasses-json-- flexible JSON serialization and deserialization of Python dataclasses. One key feature we use is the ability to omit None's (nulls) from request JSON to decrease payload size.requests -- defacto standard HTTP library used to call Promoted APIs.