Package: incountry
InCountry Storage SDKTable of contentsInstallationCountries listQuickstart guideStorage configurationExtra Storage optionsRequest OptionsoAuth options configurationEncryption key/secretServer side encryptionDebug modeUsageWriting data to storageList of available record fieldsDate fieldsBatchesReading stored dataFind recordsSortingFind error handlingFind one recordDeleting a single recordDeleting multiple recordsAttaching files to a recordAdding attachmentsDeleting attachmentsDownloading attachmentsWorking with attachment meta infoHealth checkData migration and key rotation supportAWS KMS integrationError handlingCustom encryption supportTesting LocallyInstallationTo install Python SDK, usepipenv(orpip), as follows:$ pipenv install incountryCountries ListTo get the full list of supported countries and their codes, pleasefollow this link.Quickstart guideTo access your data in InCountry Platform by using Python SDK, you need to create an instance of the Storage class. You can retrieve theclient_id,client_secretandenvironment_idvariables from your dashboard on InCountry Portal.fromincountryimportStorage,SecretKeyAccessorstorage=Storage(client_id="<client_id>",client_secret="<client_secret>",environment_id="<environment_id>",secret_key_accessor=SecretKeyAccessor(lambda:"<encryption_secret>"),)Storage ConfigurationBelow you can find a full list of possible configuration options for creating a Storage instance.classStorage:def__init__(self,# Required.# Can be set via INC_ENVIRONMENT_ID env variable.environment_id:Optional[str]=None,# Required when using oAuth authorization.# Can be set via INC_CLIENT_ID env variable.client_id:Optional[str]=None,# Required when using oAuth authorization.# Can be set via INC_CLIENT_SECRET env variable.client_secret:Optional[str]=None,# Used when OAuth token is already acquired prior to Storage initialization.# Mutually exclusive with client_id, client_secret.oauth_token:Optional[str]=None,# Instance of the SecretKeyAccessor class.# Used to fetch the encryption secret. Not compatible with SSE mode.secret_key_accessor:Optional[SecretKeyAccessor]=None,# Optional. Defines API URL.# Can be set via INC_ENDPOINT env variable.endpoint:Optional[str]=None,# Optional. Default is EncryptionType.LOCAL.# Switch encryption modes:#   EncryptionType.LOCAL - Enables encryption using SDK. Encrypted data will be sent to InCountry platform.#   EncryptionType.SSE - Enables the server side encryption (SSE) mode.#                        Unencrypted data will be sent to InCountry platform via HTTPS,#                        then encrypted and saved on the server side.#                        Enabling this option disables local (SDK-side) encryption, hashing and normalizing.#   EncryptionType.OFF - Base64 encoded not encrypted data will be sent and stored at InCountry platform# Also supports boolean values as backward compatibility:#   True = EncryptionType.LOCAL#   False = EncryptionType.OFFencrypt:Optional[Union[bool,str]]=True,# Optional. If True enables the additional debug logging.debug:Optional[bool]=False,# Optional. Used to fine-tune some configurations.options:Optional[Dict[str,Any]]={},# Optional. List of custom encryption configurations.custom_encryption_configs:Optional[List[dict]]=None,):...Extra Storage optionsYou can use theoptionsparameter to tweak some SDK configurations:storage=Storage(...,options={"http_options":{"timeout":int,# Defines http requests timeout, in seconds.# Default value is 30"retryBaseDelay":int,# Sets a custom initial http request retry delay in seconds.# Retries when receiving HTTP 429 code from the server.# Default value is 1"retryMaxDelay":int,# Sets a custom maximum retry delay in seconds.# Default value is 32},"auth_endpoints":dict,# A custom endpoints map that should be used for# fetching oAuth tokens"countries_endpoint":str,# If your PoPAPI configuration relies on a custom# PoPAPI server (rather than the default one)# use the `countries_endpoint` option to specify# the endpoint responsible for fetching the list# of supported countries"endpoint_mask":str,# Defines API base hostname part to use.# If set, all requests will be sent to# https://${country}${endpoint_mask} host# instead of the default one# (https://${country}-mt-01.api.incountry.io)"normalize_keys":bool,# If set to True all the string record keys will be stored in lowercase# (except 'body' and 'precommit_body').# Not compatible with SSE mode."hash_search_keys":bool,# If set to False key1-key20 will be stored unhashed, enabling# partial match search using 'search_keys'.# Not compatible with SSE mode.})Request OptionsEvery Storage method allows to pass custom HTTP headers viarequest_optionsparameter, e.g.:storage.write(country="us",record_key="user_1",...,request_options={"http_headers":{"request-id":"<uuid>",},},)oAuth options configurationThe SDK allows to precisely configure oAuth authorization endpoints (if needed). Use this option only if your plan configuration requires so.Below you can find the example of how to create a storage instance with custom oAuth endpoints:storage=Storage(client_id="<client_id>",client_secret="<client_secret>",environment_id="<environment_id>",secret_key_accessor=SecretKeyAccessor(lambda:"<encryption_secret>"),options={"auth_endpoints":{"default":"<default_auth_endpoint>","emea":"<auth_endpoint_for_emea_region>","apac":"<auth_endpoint_for_apac_region>","amer":"<auth_endpoint_for_amer_region>",},},)Encryption key/secretThesecret_key_accessorvariable is used to pass a key or secret used for data encryption.TheSecretKeyAccessorclass constructor allows you to pass a function that should return either a string representing your secret or a dictionary (we call itsecrets_dataobject):{"secrets":[{"secret":str,"version":int,# Should be an integer greater than or equal to 0"isKey":bool,# Should be True only for user-defined encryption keys}},....],"currentVersion":int,}Note: even though SDK uses PBKDF2 to generate a cryptographically strong encryption key, you must ensure that you provide a secret/password which follows the modern security best practices and standards.Thesecrets_datavariable allows you to specify multiple keys/secrets which SDK will use for data decryption based on the version of the key or secret used for encryption. Meanwhile SDK will encrypt data only by using a key (or secret) which matches thecurrentVersionparameter provided in thesecrets_dataobject.This enables the flexibility required to support Key Rotation policies when secrets (or keys) must be changed with time. The SDK will encrypt data by using the current secret (or key) while maintaining the ability to decrypt data records that were encrypted with old secrets (or keys). The SDK also provides a method for data migration which allows you to re-encrypt data with the newest secret (or key). For details please see themigratemethod.The SDK allows you to use custom encryption keys, instead of secrets. Please note that a user-defined encryption key should be a base64-encoded 32-bytes-long key as required by AES-256 cryptographic algorithm.Below you can find several examples of how you can useSecretKeyAccessormodule.# Get a secret from a variablefromincountryimportSecretKeyAccessorpassword="password"secret_key_accessor=SecretKeyAccessor(lambda:password)# Get secrets via an HTTP requestfromincountryimportSecretKeyAccessorimportrequestsasreqdefget_secrets_data():url="<your_secret_url>"r=req.get(url)returnr.json()# assuming response is a `secrets_data` objectsecret_key_accessor=SecretKeyAccessor(get_secrets_data)Server side encryptionSDK supports server side encryption mode. In this mode all the data encryption and hashing is performed server-side. The input data is sent to the InCountry platform as is.
Below you can find an example of how to use theEncryptionType.SSEoption:fromincountryimportStorage,EncryptionTypestorage=Storage(client_id="<client_id>",client_secret="<client_secret>",environment_id="<environment_id>",encrypt=EncryptionType.SSE,)With SSE mode enabled SDK ignores the following configuration parameters:options.normalize_keys,options.hash_search_keysandsecret_key_accessor.NOTEAvailability of the SSE mode may vary per subscription plan. Please contact the InCountry support team for details.Debug modeSDK supports debug mode allowing all SDK public methods to return raw http response along with the original return data.NoteSome methods may perform multiple requests (e.g.migrate). In that case raw http response data will be represented as alist.fromincountryimportStoragestorage=Storage(...,debug=True,...,)read_result=storage.read(...)# you can use original data as usualread_result["record"]# raw http response goes here (instance of requests.Response)read_result[Storage.HTTP_DEBUG_KEY]# e.g to access response headersread_result[Storage.HTTP_DEBUG_KEY].headers["<header_name>"]# migrate httpmigrate_result=storage.migrate(...)migrate_result[Storage.HTTP_DEBUG_KEY][0]# first request - findmigrate_result[Storage.HTTP_DEBUG_KEY][1]# second request - batch_writeUsageWriting data to StorageUse thewritemethod in order to create/replace a record (byrecord_key).defwrite(self,country:str,record_key:str,request_options:dict={},**record_data:Union[str,int])->Dict[str,TRecord]:...# write returns created record dict on success{"record":Dict}Below you can find an example of how you can use thewritemethod.write_result=storage.write(country="us",record_key="user_1",body="some PII data",profile_key="customer",range_key1=10000,key1="english",key2="rolls-royce",)# write_result would be as followswrite_result={"record":{"record_key":"user_1","body":"some PII data","profile_key":"customer","range_key1":10000,"key1":"english","key2":"rolls-royce",}}For the list of possiblerecord_datakwargs, see the section below.List of available record fieldsv3.0.0 release introduced a series of new fields available for data storage. Below you can find the full list of all the fields available for storage in InCountry Platform along with their types and storage methods. Each field is either encrypted, hashed or stored as is:# String fields, hashedrecord_keykey1key2key3key4key5key6key7key8key9key10key11key12key13key14key15key16key17key18key19key20profile_keyservice_key1service_key2service_key3service_key4service_key5parent_key# String fields, encryptedbodyprecommit_body# Date fields, plainexpires_at# Int fields, plainrange_key1range_key2range_key3range_key4range_key5range_key6range_key7range_key8range_key9range_key10Date fieldsUse thecreated_atandupdated_atfields to access date-related information about records. Thecreated_atfield stores a date when a record was initially created in the target country. Theupdated_atfield stores a date of the latest write operation for the givenrecord_key.Useexpires_atfield to limit the lifespan of a record stored in InCountry Platform. The record will be automatically deleted upon reaching the specified date.Below is an example of how to create a record that will be deleted in a day:fromdatetimeimportdatetime,timedelta,timezone...deletion_date=datetime.now(timezone.utc)+timedelta(days=1)write_result=storage.write(country="us",record_key="user_1",expires_at=deletion_date,)# write_result would be as followswrite_result={"record":{"record_key":"user_1","expires_at":datetime.datetime(2021,8,11,12,36,52,607534,tzinfo=datetime.timezone.utc),"created_at":datetime.datetime(2021,7,26,15,34,28,910266,tzinfo=datetime.timezone.utc),"updated_at":datetime.datetime(2021,8,10,12,36,54,879828,tzinfo=datetime.timezone.utc),}}NOTEAll dates are stored in UTC timezone and converted to UTC timezone in case ofexpires_at.It's highly recommended to provide timezone-aware datetime objects to avoid any timezone-related issues in future.E.g., to create an object representing the current time in UTC you can calldatetime.now(timezone.utc)Please avoid usingdatetime.utcnow()as it returns naive datetime object without timezone information, which may result in wrong timezone conversion when passed asexpires_atvalue.BatchesUse thebatch_writemethod to create/replace multiple records at once.defbatch_write(self,country:str,records:List[TRecord],request_options:dict={})->Dict[str,List[TRecord]]:...# batch_write returns the following dict of created records{"records":List}Below you can find an example of how to use this method.batch_result=storage.batch_write(country="us",records=[{"record_key":"key1","body":"body1",...},{"record_key":"key2","body":"body2",...},],)# batch_result would be as followsbatch_result={"records":[{"record_key":"key1","body":"body1",...},{"record_key":"key2","body":"body2",...},]}Reading stored dataYou can read the stored data records by itsrecord_keyusing thereadmethod.defread(self,country:str,record_key:str,request_options:dict={})->Dict[str,TRecord]:...# The read method returns the record dictionary if the record is found{"record":Dict}Thereadmethod usage is as follows:read_result=storage.read(country="us",record_key="user1")# read_result would be as followsread_result={"record":{"record_key":"user_1","body":"some PII data","profile_key":"customer","range_key1":10000,"key1":"english","key2":"rolls-royce","created_at":datetime.datetime(...),"updated_at":datetime.datetime(...),}}Find recordsYou can look up for records by keys or version using thefindmethod.deffind(self,country:str,limit:Optional[int]=FIND_LIMIT,offset:Optional[int]=0,sort:Optional[List[TSortFilter]]=None,request_options:dict={},**filters:Union[TIntFilter,TStringFilter],)->Dict[str,Any]:...Note: SDK returns 100 records at most.The returned object looks like the following:{"data":List,"errors":List,# optional"meta":{"limit":int,"offset":int,"total":int,# total records matching filter, ignoring limit}}You can use the following options to look up for records by hashed string keys from thelist above:# single valuekey1="value1"# records with key1 equal to "value1"# list of valueskey2=["value1","value2"]# records with key2 equal to "value1" or "value2"# dict with $not operatorkey3={"$not":"value1"}# records with key3 not equal "value1"key4={"$not":["value1","value2"]}# records with key4 equal to neither "value1" or "value2"You can use specialsearch_keysfilter to search records by partial match (similar toLIKESQL operator) among record's text fieldskey1, ..., key20.search_keys="text to find"NOTEsearch_keyscannot be used in combination with any ofkey1, key2, ..., key20keys and works only in combination with non-hashing Storage mode (withhash_search_keysStorage option set toFalse).Another way to find records by partial key match is using$likeoperator. It provides a partial match search (similar to theLIKESQL operator without special characters) against one of the record’s string fieldskey1, key2, ..., key20.key3={"$like":"abc"}# Matches all records where record.key3 LIKE 'abc'Note: You can use eithersearchKeysor$like, not both.hash_search_keysStorage option should also be set toFalse.You can use the following options to look up for records by int keys from thelist above:# single valuerange_key1=1# records with range_key1 equal to 1# list of valuesrange_key2=[1,2]# records with range_key2 equal to 1 or 2# dictionary with comparison operatorsrange_key3={"$gt":1}# records with range_key3 greater than 1range_key4={"$gte":1}# records with range_key4 greater than or equal to 1range_key5={"$lt":1}# records with range_key5 less than 1range_key6={"$lte":1}# records with range_key6 less than or equal to 1# you can combine different comparison operatorsrange_key7={"$gt":1,"$lte":10}# records with range_key7 greater than 1 and less than or equal to 10# you cannot combine similar comparison operators - e.g. $gt and $gte, $lt and $lteYou can use the following option to look up for records byversion(encryption key version):# single valueversion=1# records with version equal to 1# list of valuesversion=[1,2]# records with version equal to 1 or 2# dictionary with $not operatorversion={"$not":1}# records with version not equal 1version={"$not":[1,2]}# records with version equal to neither 1 or 2By default all the search keys are joined with the logicalANDoperator. You can switch to logicalORby using$oroperator:query={"$or":[{"key1":"123"},{"key2":"123"}]}find_result=storage.find(country="us",**query])Note: currently you can only use string keys inside$oroperator. Nested$oroperators are not supported as well.SortingYou can usesortparameter to sort the returned records by one or multiple keys.
SDK currently supports sorting bykey1...key20,range_key1...range_key10,created_at,updated_at, expires_atkeys.fromincountryimportSortOrder# you can use SordOrder enum class to specify sorting ordersort=[{"key1":SortOrder.ASC},{"range_key1":SortOrder.DESC},{"created_at":SortOrder.DESC}]# or you can use string valuessort=[{"key1":"asc"},{"range_key1":"desc"},{"updated_at":"asc"}]WARNINGTo usesortin find() call for string keyskey1...key20you need to sethash_search_keysoption toFalse.Below you can find an example of how you can use thefindmethod:find_result=storage.find(country="us",limit=10,offset=10,key1="value1",key2=["value2","value3"],sort=[{"key1":"asc"}])# find_result would be as followsfind_result={"data":[{"record_key":"<record_key>","body":"<body>","key1":"value1","key2":"value2","created_at":datetime.datetime(...),"updated_at":datetime.datetime(...),...}],"meta":{"limit":10,"offset":10,"total":100,}}Find error handlingThere may be a situation when thefindmethod receives records that cannot be decrypted.
For example, this may happen once the encryption key has been changed while the found data was encrypted with the older version of that key.
In such cases data returned by the find() method will be as follows:{"data":[...],# successfully decrypted records"errors":[{"rawData",# raw record which caused decryption error"error",# decryption error description},...],"meta":{...}}Find one recordIf you need to find only one of the records matching a specific filter, you can use thefind_onemethod.deffind_one(self,country:str,offset:Optional[int]=0,sort:Optional[List[TSortFilter]]=None,request_options:dict={},**filters:Union[TIntFilter,TStringFilter],)->Union[None,Dict[str,Dict]]:...# If a record is not found, the find_one method returns `None`. Otherwise it returns a record dictionary.{"record":Dict}Below you can find the example of how to use thefind_onemethod:find_one_result=storage.find_one(country="us",key1="english",key2=["rolls-royce","bmw"])# find_one_result would be as followsfind_one_result={"record":{"record_key":"user_1","body":"some PII data","profile_key":"customer","range_key1":10000,"key1":"english","key2":"rolls-royce",}}Deleting a single recordUse thedeletemethod in order to delete a record from InCountry Platform. It is only possible by using therecord_keyfield.defdelete(self,country:str,record_key:str,request_options:dict={})->Dict[str,bool]:...# the delete method returns the following dictionary upon success{"success":True}Below you can find the example of how to use the delete method:delete_result=storage.delete(country="us",record_key="<record_key>")# delete_result would be as followsdelete_result={"success":True}Deleting multiple recordsYou can use thebatch_deletemethod to delete multiple records from the InCountry platform. For now, the SDK allows deletion only by a list ofrecord_keys.defbatch_delete(self,country:str,record_key:List[str],request_options:dict={})->Dict[str,bool]:...# the batch_delete method returns the following dictionary upon success{"success":True}Below you can find the example of how to use the delete method:delete_result=storage.batch_delete(country="us",record_key=["<record_key>"])# delete_result would be as followsdelete_result={"success":True}Attaching files to a recordNOTEAttachments are currently available for InCountry dedicated instances only. Please check your subscription plan for details. This may require specifying your dedicated instance endpoint when configuring Python SDK Storage.InCountry Storage allows you to attach files to the previously created records. Attachments' meta information is available through theattachmentsfield ofrecorddictionary, returned byread,findandfind_onemethods.read_result=storage.read(country="us",record_key="<record_key>")# read_result with attachments would be as followsread_result={"record":{"record_key":"user_1",...,"attachments":[{"created_at":datetime,"updated_at":datetime,"download_link":"<download_url>","file_id":"<file_id>","filename":"<file_name>.<ext>","hash":"<hash>","mime_type":"<mime_type>","size":1,},],},}Adding attachmentsNote:Theadd_attachmentmethod allows you to add or replace attachments.
File data can be provided either asBinaryIOobject orstringwith a path to the file in the file system.defadd_attachment(self,country:str,record_key:str,file:Union[BinaryIO,str],mime_type:str=None,upsert:bool=False,request_options:dict={},)->Dict[str,Union[str,int,datetime]]:...# The add_attachment returns attachment_meta dictionary{"attachment_meta":{"created_at":datetime,"updated_at":datetime,"download_link":"<download_url>","file_id":"<file_id>","filename":"<file_name>.<ext>","hash":"<hash>","mime_type":"<mime_type>","size":1,}}Example of usage:# using file pathstorage.add_attachment(country="us",record_key="<record_key>",file="./README.md")# using BinaryIO objectwithopen("./README.md","rb")asattachment_file:storage.add_attachment(country="<country>",record_key="<record_key>",file=attachment_file)Deleting attachmentsThedelete_attachmentmethod allows you to delete attachment using itsfile_id.defdelete_attachment(self,country:str,record_key:str,file_id:str,request_options:dict={})->Dict[str,bool]::...# the delete_attachment method returns the following dictionary upon success{"success":True}Example of usage:delete_result=storage.delete_attachment(country="us",record_key="<record_key>",file_id="<file_id>")# delete_result would be as followsdelete_result={"success":True}Downloading attachmentsTheget_attachment_filemethod allows you to download attachment contents.
It returns dictionary with readable file body and filename.defget_attachment_file(self,country:str,record_key:str,file_id:str,request_options:dict={})->Dict[str,Dict]:...# the get_attachment_file method returns the following dictionary upon success{"attachment_data":{"filename":str,"file":BytesIO,}}Example of usage:get_attachment_res=storage.get_attachment_file(country="us",record_key="<record_key>",file_id="<file_id>")attachment_data=get_attachment_res["attachment_data"]withopen(attachment_data["filename"],"wb")asf:f.write(attachment_data["file"].read())Working with attachment meta infoTheget_attachment_metamethod allows you to retrieve attachment's metadata using itsfile_id.defget_attachment_meta(self,country:str,record_key:str,file_id:str,request_options:dict={})->Dict[str,Union[str,int,datetime]]:...# The get_attachment_meta returns attachment_meta dictionary{"attachment_meta":{"created_at":datetime,"updated_at":datetime,"download_link":"<download_url>","file_id":"<file_id>","filename":"<file_name>.<ext>","hash":"<hash>","mime_type":"<mime_type>","size":1,}}Example of usage:get_attachment_meta_res=storage.get_attachment_meta(country="us",record_key="<record_key>",file_id="<file_id>")Theupdate_attachment_metamethod allows you to update attachment's metadata (MIME type and file name).defupdate_attachment_meta(self,country:str,record_key:str,file_id:str,filename:str=None,mime_type:str=None,request_options:dict={},)->Dict[str,Union[str,int,datetime]]:...# The update_attachment_meta returns attachment_meta dictionary{"attachment_meta":{"created_at":datetime,"updated_at":datetime,"download_link":"<download_url>","file_id":"<file_id>","filename":"<file_name>.<ext>","hash":"<hash>","mime_type":"<mime_type>","size":1,}}Example of usage:storage.update_attachment_meta(country="us",record_key="<record_key>",file_id="<file_id>",filename="new_file_name.txt",mime_type="text/plain")Health checkThehealth_checkmethod ofStorageclass allows you to check availability of a remote storage service by country identifierdefhealth_check(self,country:str,request_options:dict={},)->Dict[str,bool]:Example of usage:response=storage.health_check(country="us")# response would be as followsresponse={"result":True}Data Migration and Key Rotation supportUsingsecret_key_accessorwhich provides thesecrets_dataobject enables key rotation and data migration support.SDK introduces themigratemethod which allows you to re-encrypt data encrypted with older versions of the secret.defmigrate(self,country:str,limit:Optional[int]=FIND_LIMIT,request_options:dict={})->Dict[str,int]:...# The migrate method returns the following dictionary with meta information{"migrated":int# the number of records migrated"total_left":int# the number of records left to migrate (number of records with version# different from `currentVersion` provided by `secret_key_accessor`)}You should specify thecountryparameter which you want to perform migration in. Additionally, you need to specify thelimitparameter for a precise number of records to migrate.Note: the maximum number of records that can be migrated per one request is 100.For a detailed example of a migration script, please see/examples/full_migration.pyAWS KMS integrationInCountry Python SDK supports usage of any 32-byte (256-bit) AES key, including ones produced by AWS KMS symmetric master key (CMK).The suggested use case assumes that AWS user already got his KMS encrypted data key (AES_256) generated. Afterwards the key gets decrypted using AWS Python client library (boto3) and then provided to InCountry Python SDK'sSecretKeyAccessor.For a detailed example of AWS KMS keys usage please seeexamples/aws-kms.pyError HandlingInCountry Python SDK may throw the following Exceptions:StorageConfigValidationException- used for Storage options validation exceptions. Can be thrown when trying to create aStorageclass instance.SecretsProviderException- can be thrown in runtime afterStorageinstance creation when an exception occurs trying to retrieve secrets.SecretsValidationException- can be thrown if secrets returned bySecretKeyAccessorinstance are in the wrong format.InputValidationException- used for input validation exceptions. Can be thrown by allStoragepublic methods.StorageClientException- used for various exceptions related to Storage misconfiguration or wrong input data provision. All of the exception classesStorageConfigValidationException,SecretsProviderException,SecretsValidationException,InputValidationExceptionare inherited fromStorageClientException.StorageServerException- thrown if SDK fails to communicate with InCountry servers or if a server response validation fails.StorageCountryNotSupportedException- thrown if InCountry platform doesn't support storage of data for the requested country. Inherited fromStorageServerException.StorageServerResponseValidationException- thrown if a server response validation fails. Inherited fromStorageServerException.StorageNetworkException- thrown if SDK failed to communicate with InCountry servers due to network issues, such as request timeout, unreachable endpoint etc. Inherited fromStorageServerException.StorageAuthenticationException- can be thrown if SDK failed to authenticate in InCountry system with the provided credentials. Inherited fromStorageServerException.StorageCryptoException- thrown during encryption/decryption procedures (both default and custom). This may be an indication of malformed/corrupted data or a wrong encryption key provided to the SDK.StorageException- general exception. Inherited by all the other exceptions.We strongly recommend you to gracefully handle all the possible exceptions:try:# use InCountry Storage instance hereexceptStorageConfigValidationExceptionase:# storage configuration errorexceptSecretsProviderExceptionase:# error trying to acquire secretsexceptSecretsValidationExceptionase:# secrets validation errorexceptInputValidationExceptionase:# input validation errorexceptStorageClientExceptionase:# general error related to user somehow provided wrong input data# when creating storage or calling a storage methodexceptStorageServerResponseValidationExceptionase:# error when trying to validate server responseexceptStorageNetworkExceptionase:# network-related errorexceptStorageAuthenticationExceptionase:# authentication errorexceptStorageCountryNotSupportedExceptionase:# requested country is not supportedexceptStorageServerExceptionase:# general error during SDK<->server communicationexceptStorageCryptoExceptionase:# some encryption errorexceptStorageExceptionase:# general errorexceptExceptionase:# something else happened not related to InCountry SDKCustom Encryption SupportSDK supports a capability to provide custom encryption/decryption methods if you decide to use your own algorithm instead of the default one.TheStorageconstructor allows you to passcustom_encryption_configsparameters as an array of custom encryption configurations within the following schema which enables custom encryption:{"encrypt":Callable,"decrypt":Callable,"isCurrent":bool,"version":str}Bothencryptanddecryptattributes should be functions implementing the following interface (with exactly the same argument names)encrypt(input:str,key:bytes,key_version:int)->str:...decrypt(input:str,key:bytes,key_version:int)->str:...They should accept raw data to encrypt/decrypt, key data (represented as bytes array) and key version received fromSecretKeyAccessor.
The resulted encrypted/decrypted data should be a string.NOTEYou should provide a specific encryption key throughsecrets_datapassed toSecretKeyAccessor. This secret should use theisForCustomEncryptionflag instead of the regularisKeyflag.secrets_data={"secrets":[{"secret":"<secret for custom encryption>","version":1,"isForCustomEncryption":True,}}],"currentVersion":1,}secret_accessor=SecretKeyAccessor(lambda:secrets_data)Theversionattribute is used to differentiate custom encryptions from each other and from the default encryption as well.
This way SDK will be able to successfully decrypt any old data if encryption changes with time.TheisCurrentattribute allows you to specify one of the custom encryption configurations which should be used for encryption. Only one encryption configuration can be set as"isCurrent": True.If none of the encryption configurations have"isCurrent": Truethen the SDK will use default encryption configuration to encrypt stored data. At the same time it will preserve the ability to decrypt old data which was encrypted with custom encryption configuration (if any).Below you can find the example of how you can set up the SDK to use custom encryption (using Fernet encryption method fromhttps://cryptography.io/en/latest/fernet/)importosfromincountryimportInCrypto,SecretKeyAccessor,Storagefromcryptography.fernetimportFernetdefenc(input:str,key:bytes,key_version:int):# Fernet cipher from cryptography package accepts keys as base64-encoded string# while InCountry SDK decodes keys as bytes# thus we need to do some bytes-to-b64 encodingcipher=Fernet(InCrypto.b_to_base64(key))returncipher.encrypt(input.encode("utf8")).decode("utf8")defdec(input:str,key:bytes,key_version:int):cipher=Fernet(InCrypto.b_to_base64(key))returncipher.decrypt(input.encode("utf8")).decode("utf8")custom_encryption_configs=[{"encrypt":enc,"decrypt":dec,"version":"test","isCurrent":True,}]key=os.urandom(InCrypto.KEY_LENGTH)# Fernet uses 32-byte length keyssecret_key_accessor=SecretKeyAccessor(lambda:{"currentVersion":1,"secrets":[{"secret":key,"version":1,"isForCustomEncryption":True}],})storage=Storage(environment_id="<env_id>",client_id="<client_id>",client_secret="<client_secret>",secret_key_accessor=secret_key_accessor,custom_encryption_configs=custom_encryption_configs,)storage.write(country="us",record_key="<record_key>",body="<body>")Testing LocallyIn terminal runpipenv run testsfor unit testsIn terminal runpipenv run integrationsto run integration tests