pax_global_header00006660000000000000000000000064135220650010014504gustar00rootroot0000000000000052 comment=2856a30a58d0b35079cb90c6624d8c070908bf57 azure-cosmos-python-3.1.1/000077500000000000000000000000001352206500100154545ustar00rootroot00000000000000azure-cosmos-python-3.1.1/.gitattributes000066400000000000000000000007431352206500100203530ustar00rootroot00000000000000# Auto detect text files and perform LF normalization * text=auto # Custom for Visual Studio *.cs diff=csharp *.sln merge=union *.csproj merge=union *.vbproj merge=union *.fsproj merge=union *.dbproj merge=union # Standard to msysgit *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain azure-cosmos-python-3.1.1/.gitignore000066400000000000000000000054061352206500100174510ustar00rootroot00000000000000RunTest.cmd ################# ## Eclipse ################# *.pydevproject .project .metadata bin/ tmp/ *.tmp *.bak *.swp *~.nib local.properties .classpath .settings/ .loadpath # External tool builders .externalToolBuilders/ # Locally stored "Eclipse launch configurations" *.launch # CDT-specific .cproject # PDT-specific .buildpath ################# ## Visual Studio ################# ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.sln.docstates # Build results [Dd]ebug/ [Rr]elease/ x64/ build/ [Bb]in/ [Oo]bj/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* *_i.c *_p.c *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.log *.scc # Visual C++ cache files ipch/ *.aps *.ncb *.opensdf *.sdf *.cachefile # Visual Studio profiler *.psess *.vsp *.vspx # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch *.ncrunch* .*crunch*.local.xml # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.Publish.xml *.pubxml # NuGet Packages Directory ## TODO: If you have NuGet Package Restore enabled, uncomment the next line #packages/ # Windows Azure Build Output csx *.build.csdef # Windows Store app package directory AppPackages/ # Others sql/ *.Cache ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.[Pp]ublish.xml *.pfx *.publishsettings # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file to a newer # Visual Studio version. Backup files are not needed, because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files App_Data/*.mdf App_Data/*.ldf ############# ## Windows detritus ############# # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Mac crap .DS_Store ############# ## Python ############# *.py[cod] # Packages *.egg *.egg-info dist/ build/ eggs/ parts/ var/ sdist/ develop-eggs/ .installed.cfg ## ignore pypi credential config file config.pypirc ##ignore python & c# vs project files *.csproj ##ignore test runner RunTest.cmd # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox #Translations *.mo #Mr Developer .mr.developer.cfg # Idea IDE .idea/* .vscode/*azure-cosmos-python-3.1.1/Contributing.md000066400000000000000000000002311352206500100204410ustar00rootroot00000000000000Please read the contributing guidelines from the [Azure Team](https://azure.microsoft.com/en-us/blog/simple-contribution-to-azure-documentation-and-sdk/)azure-cosmos-python-3.1.1/LICENSE.txt000066400000000000000000000020761352206500100173040ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Microsoft Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.azure-cosmos-python-3.1.1/MANIFEST.in000066400000000000000000000002631352206500100172130ustar00rootroot00000000000000include README.md include LICENSE.txt recursive-include doc *.bat recursive-include doc *.py recursive-include doc *.rst recursive-include doc Makefile recursive-include test *.pyazure-cosmos-python-3.1.1/README.md000066400000000000000000000031701352206500100167340ustar00rootroot00000000000000# Microsoft Azure Cosmos Python SDK Welcome to the repo containing all things Python for the Azure Cosmos DB API which is published with name [azure-cosmos](https://pypi.python.org/pypi/azure-cosmos/). For documentation please see the Microsoft Azure [link](https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-sdk-python). ## Pre-requirements Python 2.7, Python 3.3, Python 3.4, or Python 3.5 https://www.python.org/downloads/ If you use Microsoft Visual Studio as IDE (we use 2015), please install the following extension for Python. http://microsoft.github.io/PTVS/ Install Cosmos DB emulator Follow instruction at https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator ## Installation: $ python setup.py install or $ pip install azure-cosmos ## Running Testing Clone the repo ```bash git clone https://github.com/Azure/azure-cosmos-python.git cd azure-cosmos-python ``` Most of the test files under test sub-folder require you to enter your Azure Cosmos master key and host endpoint: masterKey = '[YOUR_KEY_HERE]' host = '[YOUR_ENDPOINT_HERE]' To run the tests: $ python -m unittest discover -s .\test -p "*.py" If you use Microsoft Visual Studio, open the project file python.pyproj, and run all the tests in Test Explorer. **Note:** Most of the test cases create containers in your Cosmos account. Containers are billing entities. By running these test cases, you may incur monetary costs on your account. ## Documentation generation Install Sphinx: http://sphinx-doc.org/install.html $ cd doc $ sphinx-apidoc -f -e -o api ..\azure\cosmos $ make.bat html azure-cosmos-python-3.1.1/azure/000077500000000000000000000000001352206500100166025ustar00rootroot00000000000000azure-cosmos-python-3.1.1/azure/__init__.py000066400000000000000000000000671352206500100207160ustar00rootroot00000000000000__import__('pkg_resources').declare_namespace(__name__)azure-cosmos-python-3.1.1/azure/cosmos/000077500000000000000000000000001352206500100201055ustar00rootroot00000000000000azure-cosmos-python-3.1.1/azure/cosmos/__init__.py000066400000000000000000000021171352206500100222170ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE.azure-cosmos-python-3.1.1/azure/cosmos/auth.py000066400000000000000000000140301352206500100214160ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Authorization helper functions in the Azure Cosmos database service. """ from hashlib import sha256 import hmac import azure.cosmos.http_constants as http_constants import six import base64 def GetAuthorizationHeader(cosmos_client, verb, path, resource_id_or_fullname, is_name_based, resource_type, headers): """Gets the authorization header. :param cosmos_client.CosmosClient cosmos_client: :param str verb: :param str path: :param str resource_id_or_fullname: :param str resource_type: :param dict headers: :return: The authorization headers. :rtype: dict """ # In the AuthorizationToken generation logic, lower casing of ResourceID is required as rest of the fields are lower cased # Lower casing should not be done for named based "ID", which should be used as is if resource_id_or_fullname is not None and not is_name_based: resource_id_or_fullname = resource_id_or_fullname.lower() if cosmos_client.master_key: return __GetAuthorizationTokenUsingMasterKey(verb, resource_id_or_fullname, resource_type, headers, cosmos_client.master_key) elif cosmos_client.resource_tokens: return __GetAuthorizationTokenUsingResourceTokens( cosmos_client.resource_tokens, path, resource_id_or_fullname) def __GetAuthorizationTokenUsingMasterKey(verb, resource_id_or_fullname, resource_type, headers, master_key): """Gets the authorization token using `master_key. :param str verb: :param str resource_id_or_fullname: :param str resource_type: :param dict headers: :param str master_key: :return: The authorization token. :rtype: dict """ # decodes the master key which is encoded in base64 key = base64.b64decode(master_key) # Skipping lower casing of resource_id_or_fullname since it may now contain "ID" of the resource as part of the fullname text = '{verb}\n{resource_type}\n{resource_id_or_fullname}\n{x_date}\n{http_date}\n'.format( verb=(verb.lower() or ''), resource_type=(resource_type.lower() or ''), resource_id_or_fullname=(resource_id_or_fullname or ''), x_date=headers.get(http_constants.HttpHeaders.XDate, '').lower(), http_date=headers.get(http_constants.HttpHeaders.HttpDate, '').lower()) if six.PY2: body = text.decode('utf-8') digest = hmac.new(key, body, sha256).digest() signature = digest.encode('base64') else: # python 3 support body = text.encode('utf-8') digest = hmac.new(key, body, sha256).digest() signature = base64.encodebytes(digest).decode('utf-8') master_token = 'master' token_version = '1.0' return 'type={type}&ver={ver}&sig={sig}'.format(type=master_token, ver=token_version, sig=signature[:-1]) def __GetAuthorizationTokenUsingResourceTokens(resource_tokens, path, resource_id_or_fullname): """Get the authorization token using `resource_tokens`. :param dict resource_tokens: :param str path: :param str resource_id_or_fullname: :return: The authorization token. :rtype: dict """ if resource_tokens and len(resource_tokens) > 0: # For database account access(through GetDatabaseAccount API), path and resource_id_or_fullname are '', # so in this case we return the first token to be used for creating the auth header as the service will accept any token in this case if not path and not resource_id_or_fullname: return next(six.itervalues(resource_tokens)) if resource_tokens.get(resource_id_or_fullname): return resource_tokens[resource_id_or_fullname] else: path_parts = [] if path: path_parts = path.split('/') resource_types = ['dbs', 'colls', 'docs', 'sprocs', 'udfs', 'triggers', 'users', 'permissions', 'attachments', 'media', 'conflicts', 'offers'] # Get the last resource id or resource name from the path and get it's token from resource_tokens for one_part in reversed(path_parts): if not one_part in resource_types and one_part in resource_tokens: return resource_tokens[one_part] return Noneazure-cosmos-python-3.1.1/azure/cosmos/base.py000066400000000000000000000536051352206500100214020ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Base functions in the Azure Cosmos database service. """ import base64 import datetime import json import uuid import urllib import binascii import azure.cosmos.auth as auth import azure.cosmos.documents as documents import azure.cosmos.http_constants as http_constants import azure.cosmos.constants as constants import azure.cosmos.runtime_constants as runtime_constants import six from six.moves.urllib.parse import quote as urllib_quote from six.moves import xrange def GetHeaders(cosmos_client, default_headers, verb, path, resource_id, resource_type, options, partition_key_range_id = None): """Gets HTTP request headers. :param cosmos_client.CosmosClient cosmos_client: :param dict default_headers: :param str verb: :param str path: :param str resource_id: :param str resource_type: :param dict options: :param str partition_key_range_id: :return: The HTTP request headers. :rtype: dict """ headers = dict(default_headers) options = options or {} if cosmos_client._useMultipleWriteLocations: headers[http_constants.HttpHeaders.AllowTentativeWrites] = "true" pre_trigger_include = options.get('preTriggerInclude') if pre_trigger_include: headers[http_constants.HttpHeaders.PreTriggerInclude] = ( pre_trigger_include if isinstance(pre_trigger_include, str) else (',').join(pre_trigger_include)) post_trigger_include = options.get('postTriggerInclude') if post_trigger_include: headers[http_constants.HttpHeaders.PostTriggerInclude] = ( post_trigger_include if isinstance(post_trigger_include, str) else (',').join(post_trigger_include)) if options.get('maxItemCount'): headers[http_constants.HttpHeaders.PageSize] = options['maxItemCount'] access_condition = options.get('accessCondition') if access_condition: if access_condition['type'] == 'IfMatch': headers[http_constants.HttpHeaders.IfMatch] = access_condition['condition'] else: headers[http_constants.HttpHeaders.IfNoneMatch] = access_condition['condition'] if options.get('indexingDirective'): headers[http_constants.HttpHeaders.IndexingDirective] = ( options['indexingDirective']) consistency_level = None ''' get default client consistency level''' default_client_consistency_level = headers.get(http_constants.HttpHeaders.ConsistencyLevel) ''' set consistency level. check if set via options, this will override the default ''' if options.get('consistencyLevel'): consistency_level = options['consistencyLevel'] headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level elif default_client_consistency_level is not None: consistency_level = default_client_consistency_level headers[http_constants.HttpHeaders.ConsistencyLevel] = consistency_level # figure out if consistency level for this request is session is_session_consistency = (consistency_level == documents.ConsistencyLevel.Session) # set session token if required if is_session_consistency is True and not IsMasterResource(resource_type): # if there is a token set via option, then use it to override default if options.get('sessionToken'): headers[http_constants.HttpHeaders.SessionToken] = options['sessionToken'] else: # check if the client's default consistency is session (and request consistency level is same), # then update from session container if default_client_consistency_level == documents.ConsistencyLevel.Session: # populate session token from the client's session container headers[http_constants.HttpHeaders.SessionToken] = ( cosmos_client.session.get_session_token(path)) if options.get('enableScanInQuery'): headers[http_constants.HttpHeaders.EnableScanInQuery] = ( options['enableScanInQuery']) if options.get('resourceTokenExpirySeconds'): headers[http_constants.HttpHeaders.ResourceTokenExpiry] = ( options['resourceTokenExpirySeconds']) if options.get('offerType'): headers[http_constants.HttpHeaders.OfferType] = options['offerType'] if options.get('offerThroughput'): headers[http_constants.HttpHeaders.OfferThroughput] = options['offerThroughput'] if 'partitionKey' in options: # if partitionKey value is Undefined, serialize it as {} to be consistent with other SDKs if options.get('partitionKey') is documents.Undefined: headers[http_constants.HttpHeaders.PartitionKey] = [{}] # else serialize using json dumps method which apart from regular values will serialize None into null else: headers[http_constants.HttpHeaders.PartitionKey] = json.dumps([options['partitionKey']]) if options.get('enableCrossPartitionQuery'): headers[http_constants.HttpHeaders.EnableCrossPartitionQuery] = options['enableCrossPartitionQuery'] if options.get('populateQueryMetrics'): headers[http_constants.HttpHeaders.PopulateQueryMetrics] = options['populateQueryMetrics'] if cosmos_client.master_key: headers[http_constants.HttpHeaders.XDate] = ( datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')) if cosmos_client.master_key or cosmos_client.resource_tokens: authorization = auth.GetAuthorizationHeader(cosmos_client, verb, path, resource_id, IsNameBased(resource_id), resource_type, headers) # urllib.quote throws when the input parameter is None if authorization: # -_.!~*'() are valid characters in url, and shouldn't be quoted. authorization = urllib_quote(authorization, '-_.!~*\'()') headers[http_constants.HttpHeaders.Authorization] = authorization if verb == 'post' or verb == 'put': if not headers.get(http_constants.HttpHeaders.ContentType): headers[http_constants.HttpHeaders.ContentType] = runtime_constants.MediaTypes.Json if not headers.get(http_constants.HttpHeaders.Accept): headers[http_constants.HttpHeaders.Accept] = runtime_constants.MediaTypes.Json if partition_key_range_id is not None: headers[http_constants.HttpHeaders.PartitionKeyRangeID] = partition_key_range_id if options.get('enableScriptLogging'): headers[http_constants.HttpHeaders.EnableScriptLogging] = options['enableScriptLogging'] if options.get('offerEnableRUPerMinuteThroughput'): headers[http_constants.HttpHeaders.OfferIsRUPerMinuteThroughputEnabled] = options['offerEnableRUPerMinuteThroughput'] if options.get('disableRUPerMinuteUsage'): headers[http_constants.HttpHeaders.DisableRUPerMinuteUsage] = options['disableRUPerMinuteUsage'] if options.get('changeFeed') is True: # On REST level, change feed is using IfNoneMatch/ETag instead of continuation. if_none_match_value = None if options.get('continuation'): if_none_match_value = options['continuation'] elif options.get('isStartFromBeginning') and options['isStartFromBeginning'] == False: if_none_match_value = '*' if if_none_match_value: headers[http_constants.HttpHeaders.IfNoneMatch] = if_none_match_value headers[http_constants.HttpHeaders.AIM] = http_constants.HttpHeaders.IncrementalFeedHeaderValue else: if options.get('continuation'): headers[http_constants.HttpHeaders.Continuation] = (options['continuation']) if options.get('populatePartitionKeyRangeStatistics'): headers[http_constants.HttpHeaders.PopulatePartitionKeyRangeStatistics] = options['populatePartitionKeyRangeStatistics'] if options.get('populateQuotaInfo'): headers[http_constants.HttpHeaders.PopulateQuotaInfo] = options['populateQuotaInfo'] return headers def GetResourceIdOrFullNameFromLink(resource_link): """Gets resource id or full name from resource link. :param str resource_link: :return: The resource id or full name from the resource link. :rtype: str """ # For named based, the resource link is the full name if IsNameBased(resource_link): return TrimBeginningAndEndingSlashes(resource_link) # Padding the resource link with leading and trailing slashes if not already if resource_link[-1] != '/': resource_link = resource_link + '/' if resource_link[0] != '/': resource_link = '/' + resource_link # The path will be in the form of # /[resourceType]/[resourceId]/ .... /[resourceType]/[resourceId]/ or # /[resourceType]/[resourceId]/ .... /[resourceType]/ # The result of split will be in the form of # ["", [resourceType], [resourceId] ... ,[resourceType], [resourceId], ""] # In the first case, to extract the resourceId it will the element # before last ( at length -2 ) and the the type will before it # ( at length -3 ) # In the second case, to extract the resource type it will the element # before last ( at length -2 ) path_parts = resource_link.split("/") if len(path_parts) % 2 == 0: # request in form # /[resourceType]/[resourceId]/ .... /[resourceType]/[resourceId]/. return str(path_parts[-2]) return None def GetAttachmentIdFromMediaId(media_id): """Gets attachment id from media id. :param str media_id: :return: The attachment id from the media id. :rtype: str """ altchars = '+-' if not six.PY2: altchars = altchars.encode('utf-8') # altchars for '+' and '/'. We keep '+' but replace '/' with '-' buffer = base64.b64decode(str(media_id), altchars) resoure_id_length = 20 attachment_id = '' if len(buffer) > resoure_id_length: # We are cutting off the storage index. attachment_id = base64.b64encode(buffer[0:resoure_id_length], altchars) if not six.PY2: attachment_id = attachment_id.decode('utf-8') else: attachment_id = media_id return attachment_id def GenerateGuidId(): """Gets a random GUID. Note that here we use python's UUID generation library. Basically UUID is the same as GUID when represented as a string. :return: The generated random GUID. :rtype: str """ return str(uuid.uuid4()) def GetPathFromLink(resource_link, resource_type=''): """Gets path from resource link with optional resource type :param str resource_link: :param str resource_type: :return: Path from resource link with resource type appended (if provided). :rtype: str """ resource_link = TrimBeginningAndEndingSlashes(resource_link) if IsNameBased(resource_link): # Replace special characters in string using the %xx escape. For example, space(' ') would be replaced by %20 # This function is intended for quoting the path section of the URL and excludes '/' to be quoted as that's the default safe char resource_link = urllib_quote(resource_link) # Padding leading and trailing slashes to the path returned both for name based and resource id based links if resource_type: return '/' + resource_link + '/' + resource_type + '/' else: return '/' + resource_link + '/' def IsNameBased(link): """Finds whether the link is name based or not :param str link: :return: True if link is name-based; otherwise, False. :rtype: boolean """ if not link: return False # trimming the leading "/" if link.startswith('/') and len(link) > 1: link = link[1:] # Splitting the link(separated by "/") into parts parts = link.split('/') # First part should be "dbs" if len(parts) == 0 or not parts[0] or not parts[0].lower() == 'dbs': return False # The second part is the database id(ResourceID or Name) and cannot be empty if len(parts) < 2 or not parts[1]: return False # Either ResourceID or database name databaseID = parts[1] # Length of databaseID(in case of ResourceID) is always 8 if len(databaseID) != 8: return True return not IsValidBase64String(str(databaseID)) def IsMasterResource(resourceType): return (resourceType == http_constants.ResourceType.Offer or resourceType == http_constants.ResourceType.Database or resourceType == http_constants.ResourceType.User or resourceType == http_constants.ResourceType.Permission or resourceType == http_constants.ResourceType.Topology or resourceType == http_constants.ResourceType.DatabaseAccount or resourceType == http_constants.ResourceType.PartitionKeyRange or resourceType == http_constants.ResourceType.Collection) def IsDatabaseLink(link): """Finds whether the link is a database Self Link or a database ID based link :param str link: Link to analyze :return: True or False. :rtype: boolean """ if not link: return False # trimming the leading and trailing "/" from the input string link = TrimBeginningAndEndingSlashes(link) # Splitting the link(separated by "/") into parts parts = link.split('/') if len(parts) != 2: return False # First part should be "dbs" if not parts[0] or not parts[0].lower() == 'dbs': return False # The second part is the database id(ResourceID or Name) and cannot be empty if not parts[1]: return False return True def IsItemContainerLink(link): """Finds whether the link is a document colllection Self Link or a document colllection ID based link :param str link: Link to analyze :return: True or False. :rtype: boolean """ if not link: return False # trimming the leading and trailing "/" from the input string link = TrimBeginningAndEndingSlashes(link) # Splitting the link(separated by "/") into parts parts = link.split('/') if len(parts) != 4: return False # First part should be "dbs" if not parts[0] or not parts[0].lower() == 'dbs': return False # Third part should be "colls" if not parts[2] or not parts[2].lower() == 'colls': return False # The second part is the database id(ResourceID or Name) and cannot be empty if not parts[1]: return False # The fourth part is the document collection id(ResourceID or Name) and cannot be empty if not parts[3]: return False return True def GetItemContainerInfo(self_link, alt_content_path, id_from_response): """ Given the self link and alt_content_path from the reponse header and result extract the collection name and collection id Ever response header has alt-content-path that is the owner's path in ascii. For document create / update requests, this can be used to get the collection name, but for collection create response, we can't use it. So we also rely on :param str self_link: Self link of the resource, as obtained from response result. :param str alt_content_path: Owner path of the resource, as obtained from response header. :param str resource_id: 'id' as returned from the response result. This is only used if it is deduced that the request was to create a collection. :return: tuple of (collection rid, collection name) :rtype: tuple """ self_link = TrimBeginningAndEndingSlashes(self_link) + '/' index = IndexOfNth(self_link, '/', 4) if index != -1: collection_id = self_link[0:index] if 'colls' in self_link: # this is a collection request index_second_slash = IndexOfNth(alt_content_path, '/', 2) if index_second_slash == -1: collection_name = alt_content_path + '/colls/' + urllib_quote(id_from_response) return collection_id, collection_name else: collection_name = alt_content_path return collection_id, collection_name else: raise ValueError('Response Not from Server Partition, self_link: {0}, alt_content_path: {1}, id: {2}' .format(self_link, alt_content_path, id_from_response)) else: raise ValueError('Unable to parse document collection link from ' + self_link) def GetItemContainerLink(link): """Gets the document collection link :param str link: Resource link :return: Document collection link. :rtype: str """ link = TrimBeginningAndEndingSlashes(link) + '/' index = IndexOfNth(link, '/', 4) if index != -1: return link[0:index] else: raise ValueError('Unable to parse document collection link from ' + link) def IndexOfNth(s, value, n): """Gets the index of Nth occurance of a given character in a string :param str s: Input string :param char value: Input char to be searched. :param int n: Nth occurrence of char to be searched. :return: Index of the Nth occurrence in the string. :rtype: int """ remaining = n for i in xrange(0, len(s)): if s[i] == value: remaining -= 1 if remaining == 0: return i return -1 def IsValidBase64String(string_to_validate): """Verifies if a string is a valid Base64 encoded string, after replacing '-' with '/' :param string string_to_validate: String to validate. :return: Whether given string is a valid base64 string or not. :rtype: str """ # '-' is not supported char for decoding in Python(same as C# and Java) which has similar logic while parsing ResourceID generated by backend try: buffer = base64.standard_b64decode(string_to_validate.replace('-', '/')) if len(buffer) != 4: return False except Exception as e: if six.PY2: e = e.message if type(e) == binascii.Error: return False else: raise e return True def TrimBeginningAndEndingSlashes(path): """Trims beginning and ending slashes :param str path: :return: Path with beginning and ending slashes trimmed. :rtype: str """ if path.startswith('/'): # Returns substring starting from index 1 to end of the string path = path[1:] if path.endswith('/'): # Returns substring starting from beginning to last but one char in the string path = path[:-1] return path # Parses the paths into a list of token each representing a property def ParsePaths(paths): if len(paths) != 1: raise ValueError("Unsupported paths count.") segmentSeparator = '/' path = paths[0] tokens = [] currentIndex = 0 while currentIndex < len(path): if path[currentIndex] != segmentSeparator: raise ValueError("Invalid path character at index " + currentIndex) currentIndex += 1 if currentIndex == len(path): break # " and ' are treated specially in the sense that they can have the / (segment separator) between them which is considered part of the token if path[currentIndex] == '\"' or path[currentIndex] == '\'': quote = path[currentIndex] newIndex = currentIndex + 1 while True: newIndex = path.find(quote, newIndex) if newIndex == -1: raise ValueError("Invalid path character at index " + currentIndex) # check if the quote itself is escaped by a preceding \ in which case it's part of the token if path[newIndex - 1] != '\\': break newIndex += 1 # This will extract the token excluding the quote chars token = path[currentIndex+1:newIndex] tokens.append(token) currentIndex = newIndex + 1 else: newIndex = path.find(segmentSeparator, currentIndex) token = None if newIndex == -1: # This will extract the token from currentIndex to end of the string token = path[currentIndex:] currentIndex = len(path) else: # This will extract the token from currentIndex to the char before the segmentSeparator token = path[currentIndex:newIndex] currentIndex = newIndex token = token.strip() tokens.append(token) return tokens azure-cosmos-python-3.1.1/azure/cosmos/consistent_hash_ring.py000066400000000000000000000122741352206500100247000ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for consistent hash ring implementation in the Azure Cosmos database service. """ import azure.cosmos.partition as partition from struct import unpack import six from six.moves import xrange class _ConsistentHashRing(object): """The ConsistentHashRing class implements a consistent hash ring using the hash generator specified. """ def __init__(self, collection_links, partitions_per_node, hash_generator): """ :param list collection_links: The links of collections participating in partitioning. :param int partitions_per_node: The partitions per node. :param HashGenerator hash_generator: The hash generator to be used for hashing algorithm. """ if collection_links is None: raise ValueError("collection_links is None.") if partitions_per_node <= 0 : raise ValueError("The partitions per node must greater than 0.") if hash_generator is None: raise ValueError("hash_generator is None.") self.collection_links = collection_links self.hash_generator = hash_generator self.partitions = self._ConstructPartitions(self.collection_links, partitions_per_node) def GetCollectionNode(self, partition_key): """Gets the SelfLink/ID based link of the collection node that maps to the partition key based on the hashing algorithm used for finding the node in the ring. :param str partition_key: The partition key to be used for finding the node in the ring. :return: The name of the collection mapped to that partition. :rtype: str """ if partition_key is None: raise ValueError("partition_key is None or empty.") partition_number = self._FindPartition(self._GetBytes(partition_key)) return self.partitions[partition_number].GetNode() def _ConstructPartitions(self, collection_links, partitions_per_node): """Constructs the partitions in the consistent ring by assigning them to collection nodes using the hashing algorithm and then finally sorting the partitions based on the hash value. """ collections_node_count = len(collection_links) partitions = [partition._Partition() for _ in xrange(0, partitions_per_node * collections_node_count)] index = 0 for collection_node in collection_links: hash_value = self.hash_generator.ComputeHash(self._GetBytes(collection_node)) for _ in xrange(0, partitions_per_node): partitions[index] = partition._Partition(hash_value, collection_node) index += 1 hash_value = self.hash_generator.ComputeHash(hash_value) partitions.sort() return partitions def _FindPartition(self, key): """Finds the partition from the byte array representation of the partition key. """ hash_value = self.hash_generator.ComputeHash(key) return self._LowerBoundSearch(self.partitions, hash_value) def _GetSerializedPartitionList(self): """Gets the serialized version of the ConsistentRing. Added this helper for the test code. """ partition_list = list() for part in self.partitions: partition_list.append((part.node, unpack(" 0: return i return len(partitions) - 1azure-cosmos-python-3.1.1/azure/cosmos/constants.py000066400000000000000000000032141352206500100224730ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Class for defining internal constants in the Azure Cosmos database service. """ class _Constants(object): """Constants used in the azure-cosmos package""" UserConsistencyPolicy = 'userConsistencyPolicy' #GlobalDB related constants WritableLocations = 'writableLocations' ReadableLocations = 'readableLocations' Name = 'name' DatabaseAccountEndpoint = 'databaseAccountEndpoint' DefaultUnavailableLocationExpirationTime = 5 * 60 * 1000 #ServiceDocument Resource EnableMultipleWritableLocations = "enableMultipleWriteLocations"azure-cosmos-python-3.1.1/azure/cosmos/cosmos_client.py000066400000000000000000003143621352206500100233310ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Document client class for the Azure Cosmos database service. """ import requests import six import azure.cosmos.base as base import azure.cosmos.documents as documents import azure.cosmos.constants as constants import azure.cosmos.http_constants as http_constants import azure.cosmos.query_iterable as query_iterable import azure.cosmos.runtime_constants as runtime_constants import azure.cosmos.request_object as request_object import azure.cosmos.synchronized_request as synchronized_request import azure.cosmos.global_endpoint_manager as global_endpoint_manager import azure.cosmos.routing.routing_map_provider as routing_map_provider import azure.cosmos.session as session import azure.cosmos.utils as utils import os class CosmosClient(object): """Represents a document client. Provides a client-side logical representation of the Azure Cosmos service. This client is used to configure and execute requests against the service. The service client encapsulates the endpoint and credentials used to access the Azure Cosmos service. """ class _QueryCompatibilityMode: Default = 0 Query = 1 SqlQuery = 2 # default number precisions _DefaultNumberHashPrecision = 3 _DefaultNumberRangePrecision = -1 # default string precision _DefaultStringHashPrecision = 3 _DefaultStringRangePrecision = -1 def __init__(self, url_connection, auth, connection_policy=None, consistency_level=documents.ConsistencyLevel.Session): """ :param str url_connection: The URL for connecting to the DB server. :param dict auth: Contains 'masterKey' or 'resourceTokens', where auth['masterKey'] is the default authorization key to use to create the client, and auth['resourceTokens'] is the alternative authorization key. :param documents.ConnectionPolicy connection_policy: The connection policy for the client. :param documents.ConsistencyLevel consistency_level: The default consistency policy for client operations. if url_connection and auth are not provided, COSMOS_ENDPOINT and COSMOS_KEY environment variables will be used. """ self.url_connection = url_connection or os.environ.get('COSMOS_ENDPOINT') self.master_key = None self.resource_tokens = None if auth is not None: self.master_key = auth.get('masterKey') self.resource_tokens = auth.get('resourceTokens') if auth.get('permissionFeed'): self.resource_tokens = {} for permission_feed in auth['permissionFeed']: resource_parts = permission_feed['resource'].split('/') id = resource_parts[-1] self.resource_tokens[id] = permission_feed['_token'] else: self.master_key = os.environ.get('COSMOS_KEY') self.connection_policy = (connection_policy or documents.ConnectionPolicy()) self.partition_resolvers = {} self.partition_key_definition_cache = {} self.default_headers = { http_constants.HttpHeaders.CacheControl: 'no-cache', http_constants.HttpHeaders.Version: http_constants.Versions.CurrentVersion, http_constants.HttpHeaders.UserAgent: utils._get_user_agent(), # For single partition query with aggregate functions we would try to accumulate the results on the SDK. # We need to set continuation as not expected. http_constants.HttpHeaders.IsContinuationExpected: False } if consistency_level != None: self.default_headers[ http_constants.HttpHeaders.ConsistencyLevel] = consistency_level # Keeps the latest response headers from server. self.last_response_headers = None if consistency_level == documents.ConsistencyLevel.Session: '''create a session - this is maintained only if the default consistency level on the client is set to session, or if the user explicitly sets it as a property via setter''' self.session = session.Session(self.url_connection) else: self.session = None self._useMultipleWriteLocations = False self._global_endpoint_manager = global_endpoint_manager._GlobalEndpointManager(self) # creating a requests session used for connection pooling and re-used by all requests self._requests_session = requests.Session() if self.connection_policy.ProxyConfiguration and self.connection_policy.ProxyConfiguration.Host: host = connection_policy.ProxyConfiguration.Host url = six.moves.urllib.parse.urlparse(host) proxy = host if url.port else host + ":" + str(connection_policy.ProxyConfiguration.Port) proxyDict = {url.scheme : proxy} self._requests_session.proxies.update(proxyDict) # Query compatibility mode. # Allows to specify compatibility mode used by client when making query requests. Should be removed when # application/sql is no longer supported. self._query_compatibility_mode = CosmosClient._QueryCompatibilityMode.Default # Routing map provider self._routing_map_provider = routing_map_provider._SmartRoutingMapProvider(self) database_account = self._global_endpoint_manager._GetDatabaseAccount() self._global_endpoint_manager.force_refresh(database_account) @property def Session(self): """ Gets the session object from the client """ return self.session @Session.setter def Session(self, session): """ Sets a session object on the document client This will override the existing session """ self.session = session @property def WriteEndpoint(self): """Gets the curent write endpoint for a geo-replicated database account. """ return self._global_endpoint_manager.get_write_endpoint() @property def ReadEndpoint(self): """Gets the curent read endpoint for a geo-replicated database account. """ return self._global_endpoint_manager.get_read_endpoint() def RegisterPartitionResolver(self, database_link, partition_resolver): """Registers the partition resolver associated with the database link :param str database_link: Database Self Link or ID based link. :param object partition_resolver: An instance of PartitionResolver. """ if not database_link: raise ValueError("database_link is None or empty.") if partition_resolver is None: raise ValueError("partition_resolver is None.") self.partition_resolvers = {base.TrimBeginningAndEndingSlashes(database_link): partition_resolver} def GetPartitionResolver(self, database_link): """Gets the partition resolver associated with the database link :param str database_link: Database self link or ID based link. :return: An instance of PartitionResolver. :rtype: object """ if not database_link: raise ValueError("database_link is None or empty.") return self.partition_resolvers.get(base.TrimBeginningAndEndingSlashes(database_link)) def CreateDatabase(self, database, options=None): """Creates a database. :param dict database: The Azure Cosmos database to create. :param dict options: The request options for the request. :return: The Database that was created. :rtype: dict """ if options is None: options = {} CosmosClient.__ValidateResource(database) path = '/dbs' return self.Create(database, path, 'dbs', None, None, options) def ReadDatabase(self, database_link, options=None): """Reads a database. :param str database_link: The link to the database. :param dict options: The request options for the request. :return: The Database that was read. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(database_link) database_id = base.GetResourceIdOrFullNameFromLink(database_link) return self.Read(path, 'dbs', database_id, None, options) def ReadDatabases(self, options=None): """Reads all databases. :param dict options: The request options for the request. :return: Query Iterable of Databases. :rtype: query_iterable.QueryIterable """ if options is None: options = {} return self.QueryDatabases(None, options) def QueryDatabases(self, query, options=None): """Queries databases. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of Databases. :rtype: query_iterable.QueryIterable """ if options is None: options = {} def fetch_fn(options): return self.__QueryFeed('/dbs', 'dbs', '', lambda r: r['Databases'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def ReadContainers(self, database_link, options=None): """Reads all collections in a database. :param str database_link: The link to the database. :param dict options: The request options for the request. :return: Query Iterable of Collections. :rtype: query_iterable.QueryIterable """ if options is None: options = {} return self.QueryContainers(database_link, None, options) def QueryContainers(self, database_link, query, options=None): """Queries collections in a database. :param str database_link: The link to the database. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of Collections. :rtype: query_iterable.QueryIterable """ if options is None: options = {} path = base.GetPathFromLink(database_link, 'colls') database_id = base.GetResourceIdOrFullNameFromLink(database_link) def fetch_fn(options): return self.__QueryFeed(path, 'colls', database_id, lambda r: r['DocumentCollections'], lambda _, body: body, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def CreateContainer(self, database_link, collection, options=None): """Creates a collection in a database. :param str database_link: The link to the database. :param dict collection: The Azure Cosmos collection to create. :param dict options: The request options for the request. :return: The Collection that was created. :rtype: dict """ if options is None: options = {} CosmosClient.__ValidateResource(collection) path = base.GetPathFromLink(database_link, 'colls') database_id = base.GetResourceIdOrFullNameFromLink(database_link) return self.Create(collection, path, 'colls', database_id, None, options) def ReplaceContainer(self, collection_link, collection, options=None): """Replaces a collection and return it. :param str collection_link: The link to the collection entity. :param dict collection: The collection to be used. :param dict options: The request options for the request. :return: The new Collection. :rtype: dict """ if options is None: options = {} CosmosClient.__ValidateResource(collection) path = base.GetPathFromLink(collection_link) collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) return self.Replace(collection, path, 'colls', collection_id, None, options) def ReadContainer(self, collection_link, options=None): """Reads a collection. :param str collection_link: The link to the document collection. :param dict options: The request options for the request. :return: The read Collection. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(collection_link) collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) return self.Read(path, 'colls', collection_id, None, options) def CreateUser(self, database_link, user, options=None): """Creates a user. :param str database_link: The link to the database. :param dict user: The Azure Cosmos user to create. :param dict options: The request options for the request. :return: The created User. :rtype: dict """ if options is None: options = {} database_id, path = self._GetDatabaseIdWithPathForUser(database_link, user) return self.Create(user, path, 'users', database_id, None, options) def UpsertUser(self, database_link, user, options=None): """Upserts a user. :param str database_link: The link to the database. :param dict user: The Azure Cosmos user to upsert. :param dict options: The request options for the request. :return: The upserted User. :rtype: dict """ if options is None: options = {} database_id, path = self._GetDatabaseIdWithPathForUser(database_link, user) return self.Upsert(user, path, 'users', database_id, None, options) def _GetDatabaseIdWithPathForUser(self, database_link, user): CosmosClient.__ValidateResource(user) path = base.GetPathFromLink(database_link, 'users') database_id = base.GetResourceIdOrFullNameFromLink(database_link) return database_id, path def ReadUser(self, user_link, options=None): """Reads a user. :param str user_link: The link to the user entity. :param dict options: The request options for the request. :return: The read User. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(user_link) user_id = base.GetResourceIdOrFullNameFromLink(user_link) return self.Read(path, 'users', user_id, None, options) def ReadUsers(self, database_link, options=None): """Reads all users in a database. :params str database_link: The link to the database. :params dict options: The request options for the request. :return: Query iterable of Users. :rtype: query_iterable.QueryIterable """ if options is None: options = {} return self.QueryUsers(database_link, None, options) def QueryUsers(self, database_link, query, options=None): """Queries users in a database. :param str database_link: The link to the database. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of Users. :rtype: query_iterable.QueryIterable """ if options is None: options = {} path = base.GetPathFromLink(database_link, 'users') database_id = base.GetResourceIdOrFullNameFromLink(database_link) def fetch_fn(options): return self.__QueryFeed(path, 'users', database_id, lambda r: r['Users'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def DeleteDatabase(self, database_link, options=None): """Deletes a database. :param str database_link: The link to the database. :param dict options: The request options for the request. :return: The deleted Database. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(database_link) database_id = base.GetResourceIdOrFullNameFromLink(database_link) return self.DeleteResource(path, 'dbs', database_id, None, options) def CreatePermission(self, user_link, permission, options=None): """Creates a permission for a user. :param str user_link: The link to the user entity. :param dict permission: The Azure Cosmos user permission to create. :param dict options: The request options for the request. :return: The created Permission. :rtype: dict """ if options is None: options = {} path, user_id = self._GetUserIdWithPathForPermission(permission, user_link) return self.Create(permission, path, 'permissions', user_id, None, options) def UpsertPermission(self, user_link, permission, options=None): """Upserts a permission for a user. :param str user_link: The link to the user entity. :param dict permission: The Azure Cosmos user permission to upsert. :param dict options: The request options for the request. :return: The upserted permission. :rtype: dict """ if options is None: options = {} path, user_id = self._GetUserIdWithPathForPermission(permission, user_link) return self.Upsert(permission, path, 'permissions', user_id, None, options) def _GetUserIdWithPathForPermission(self, permission, user_link): CosmosClient.__ValidateResource(permission) path = base.GetPathFromLink(user_link, 'permissions') user_id = base.GetResourceIdOrFullNameFromLink(user_link) return path, user_id def ReadPermission(self, permission_link, options=None): """Reads a permission. :param str permission_link: The link to the permission. :param dict options: The request options for the request. :return: The read permission. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(permission_link) permission_id = base.GetResourceIdOrFullNameFromLink(permission_link) return self.Read(path, 'permissions', permission_id, None, options) def ReadPermissions(self, user_link, options=None): """Reads all permissions for a user. :param str user_link: The link to the user entity. :param dict options: The request options for the request. :return: Query Iterable of Permissions. :rtype: query_iterable.QueryIterable """ if options is None: options = {} return self.QueryPermissions(user_link, None, options) def QueryPermissions(self, user_link, query, options=None): """Queries permissions for a user. :param str user_link: The link to the user entity. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of Permissions. :rtype: query_iterable.QueryIterable """ if options is None: options = {} path = base.GetPathFromLink(user_link, 'permissions') user_id = base.GetResourceIdOrFullNameFromLink(user_link) def fetch_fn(options): return self.__QueryFeed(path, 'permissions', user_id, lambda r: r['Permissions'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def ReplaceUser(self, user_link, user, options=None): """Replaces a user and return it. :param str user_link: The link to the user entity. :param dict user: :param dict options: The request options for the request. :return: The new User. :rtype: dict """ if options is None: options = {} CosmosClient.__ValidateResource(user) path = base.GetPathFromLink(user_link) user_id = base.GetResourceIdOrFullNameFromLink(user_link) return self.Replace(user, path, 'users', user_id, None, options) def DeleteUser(self, user_link, options=None): """Deletes a user. :param str user_link: The link to the user entity. :param dict options: The request options for the request. :return: The deleted user. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(user_link) user_id = base.GetResourceIdOrFullNameFromLink(user_link) return self.DeleteResource(path, 'users', user_id, None, options) def ReplacePermission(self, permission_link, permission, options=None): """Replaces a permission and return it. :param str permission_link: The link to the permission. :param dict permission: :param dict options: The request options for the request. :return: The new Permission. :rtype: dict """ if options is None: options = {} CosmosClient.__ValidateResource(permission) path = base.GetPathFromLink(permission_link) permission_id = base.GetResourceIdOrFullNameFromLink(permission_link) return self.Replace(permission, path, 'permissions', permission_id, None, options) def DeletePermission(self, permission_link, options=None): """Deletes a permission. :param str permission_link: The link to the permission. :param dict options: The request options for the request. :return: The deleted Permission. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(permission_link) permission_id = base.GetResourceIdOrFullNameFromLink(permission_link) return self.DeleteResource(path, 'permissions', permission_id, None, options) def ReadItems(self, collection_link, feed_options=None): """Reads all documents in a collection. :param str collection_link: The link to the document collection. :param dict feed_options: :return: Query Iterable of Documents. :rtype: query_iterable.QueryIterable """ if feed_options is None: feed_options = {} return self.QueryItems(collection_link, None, feed_options) def QueryItems(self, database_or_Container_link, query, options=None, partition_key=None): """Queries documents in a collection. :param str database_or_Container_link: The link to the database when using partitioning, otherwise link to the document collection. :param (str or dict) query: :param dict options: The request options for the request. :param str partition_key: Partition key for the query(default value None) :return: Query Iterable of Documents. :rtype: query_iterable.QueryIterable """ database_or_Container_link = base.TrimBeginningAndEndingSlashes(database_or_Container_link) if options is None: options = {} if(base.IsDatabaseLink(database_or_Container_link)): # Python doesn't have a good way of specifying an overloaded constructor, and this is how it's generally overloaded constructors are specified(by calling a @classmethod) and returning the 'self' instance return query_iterable.QueryIterable.PartitioningQueryIterable(self, query, options, database_or_Container_link, partition_key) else: path = base.GetPathFromLink(database_or_Container_link, 'docs') collection_id = base.GetResourceIdOrFullNameFromLink(database_or_Container_link) def fetch_fn(options): return self.__QueryFeed(path, 'docs', collection_id, lambda r: r['Documents'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn, database_or_Container_link) def QueryItemsChangeFeed(self, collection_link, options=None): """Queries documents change feed in a collection. :param str collection_link: The link to the document collection. :param dict options: The request options for the request. options may also specify partition key range id. :return: Query Iterable of Documents. :rtype: query_iterable.QueryIterable """ partition_key_range_id = None if options is not None and 'partitionKeyRangeId' in options: partition_key_range_id = options['partitionKeyRangeId'] return self._QueryChangeFeed(collection_link, "Documents" , options, partition_key_range_id) def _QueryChangeFeed(self, collection_link, resource_type, options=None, partition_key_range_id=None): """Queries change feed of a resource in a collection. :param str collection_link: The link to the document collection. :param str resource_type: The type of the resource. :param dict options: The request options for the request. :param str partition_key_range_id: Specifies partition key range id. :return: Query Iterable of Documents. :rtype: query_iterable.QueryIterable """ if options is None: options = {} options['changeFeed'] = True resource_key_map = {'Documents' : 'docs'} # For now, change feed only supports Documents and Partition Key Range resouce type if resource_type not in resource_key_map: raise NotImplementedError(resource_type + " change feed query is not supported.") resource_key = resource_key_map[resource_type] path = base.GetPathFromLink(collection_link, resource_key) collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) def fetch_fn(options): return self.__QueryFeed(path, resource_key, collection_id, lambda r: r[resource_type], lambda _, b: b, None, options, partition_key_range_id), self.last_response_headers return query_iterable.QueryIterable(self, None, options, fetch_fn, collection_link) def _ReadPartitionKeyRanges(self, collection_link, feed_options=None): """Reads Partition Key Ranges. :param str collection_link: The link to the document collection. :param dict feed_options: :return: Query Iterable of PartitionKeyRanges. :rtype: query_iterable.QueryIterable """ if feed_options is None: feed_options = {} return self._QueryPartitionKeyRanges(collection_link, None, feed_options) def _QueryPartitionKeyRanges(self, collection_link, query, options=None): """Queries Partition Key Ranges in a collection. :param str collection_link: The link to the document collection. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of PartitionKeyRanges. :rtype: query_iterable.QueryIterable """ if options is None: options = {} path = base.GetPathFromLink(collection_link, 'pkranges') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) def fetch_fn(options): return self.__QueryFeed(path, 'pkranges', collection_id, lambda r: r['PartitionKeyRanges'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def CreateItem(self, database_or_Container_link, document, options=None): """Creates a document in a collection. :param str database_or_Container_link: The link to the database when using partitioning, otherwise link to the document collection. :param dict document: The Azure Cosmos document to create. :param dict options: The request options for the request. :param bool options['disableAutomaticIdGeneration']: Disables the automatic id generation. If id is missing in the body and this option is true, an error will be returned. :return: The created Document. :rtype: dict """ # Python's default arguments are evaluated once when the function is defined, not each time the function is called (like it is in say, Ruby). # This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well. # So, using a non-mutable deafult in this case(None) and assigning an empty dict(mutable) inside the method # For more details on this gotcha, please refer http://docs.python-guide.org/en/latest/writing/gotchas/ if options is None: options = {} # We check the link to be document collection link since it can be database link in case of client side partitioning if(base.IsItemContainerLink(database_or_Container_link)): options = self._AddPartitionKey(database_or_Container_link, document, options) collection_id, document, path = self._GetContainerIdWithPathForItem(database_or_Container_link, document, options) return self.Create(document, path, 'docs', collection_id, None, options) def UpsertItem(self, database_or_Container_link, document, options=None): """Upserts a document in a collection. :param str database_or_Container_link: The link to the database when using partitioning, otherwise link to the document collection. :param dict document: The Azure Cosmos document to upsert. :param dict options: The request options for the request. :param bool options['disableAutomaticIdGeneration']: Disables the automatic id generation. If id is missing in the body and this option is true, an error will be returned. :return: The upserted Document. :rtype: dict """ # Python's default arguments are evaluated once when the function is defined, not each time the function is called (like it is in say, Ruby). # This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well. # So, using a non-mutable deafult in this case(None) and assigning an empty dict(mutable) inside the method # For more details on this gotcha, please refer http://docs.python-guide.org/en/latest/writing/gotchas/ if options is None: options = {} # We check the link to be document collection link since it can be database link in case of client side partitioning if(base.IsItemContainerLink(database_or_Container_link)): options = self._AddPartitionKey(database_or_Container_link, document, options) collection_id, document, path = self._GetContainerIdWithPathForItem(database_or_Container_link, document, options) return self.Upsert(document, path, 'docs', collection_id, None, options) PartitionResolverErrorMessage = "Couldn't find any partition resolvers for the database link provided. Ensure that the link you used when registering the partition resolvers matches the link provided or you need to register both types of database link(self link as well as ID based link)." # Gets the collection id and path for the document def _GetContainerIdWithPathForItem(self, database_or_Container_link, document, options): if not database_or_Container_link: raise ValueError("database_or_Container_link is None or empty.") if document is None: raise ValueError("document is None.") CosmosClient.__ValidateResource(document) document = document.copy() if (not document.get('id') and not options.get('disableAutomaticIdGeneration')): document['id'] = base.GenerateGuidId() collection_link = database_or_Container_link if(base.IsDatabaseLink(database_or_Container_link)): partition_resolver = self.GetPartitionResolver(database_or_Container_link) if(partition_resolver != None): collection_link = partition_resolver.ResolveForCreate(document) else: raise ValueError(CosmosClient.PartitionResolverErrorMessage) path = base.GetPathFromLink(collection_link, 'docs') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) return collection_id, document, path def ReadItem(self, document_link, options=None): """Reads a document. :param str document_link: The link to the document. :param dict options: The request options for the request. :return: The read Document. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(document_link) document_id = base.GetResourceIdOrFullNameFromLink(document_link) return self.Read(path, 'docs', document_id, None, options) def ReadTriggers(self, collection_link, options=None): """Reads all triggers in a collection. :param str collection_link: The link to the document collection. :param dict options: The request options for the request. :return: Query Iterable of Triggers. :rtype: query_iterable.QueryIterable """ if options is None: options = {} return self.QueryTriggers(collection_link, None, options) def QueryTriggers(self, collection_link, query, options=None): """Queries triggers in a collection. :param str collection_link: The link to the document collection. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of Triggers. :rtype: query_iterable.QueryIterable """ if options is None: options = {} path = base.GetPathFromLink(collection_link, 'triggers') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) def fetch_fn(options): return self.__QueryFeed(path, 'triggers', collection_id, lambda r: r['Triggers'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def CreateTrigger(self, collection_link, trigger, options=None): """Creates a trigger in a collection. :param str collection_link: The link to the document collection. :param dict trigger: :param dict options: The request options for the request. :return: The created Trigger. :rtype: dict """ if options is None: options = {} collection_id, path, trigger = self._GetContainerIdWithPathForTrigger(collection_link, trigger) return self.Create(trigger, path, 'triggers', collection_id, None, options) def UpsertTrigger(self, collection_link, trigger, options=None): """Upserts a trigger in a collection. :param str collection_link: The link to the document collection. :param dict trigger: :param dict options: The request options for the request. :return: The upserted Trigger. :rtype: dict """ if options is None: options = {} collection_id, path, trigger = self._GetContainerIdWithPathForTrigger(collection_link, trigger) return self.Upsert(trigger, path, 'triggers', collection_id, None, options) def _GetContainerIdWithPathForTrigger(self, collection_link, trigger): CosmosClient.__ValidateResource(trigger) trigger = trigger.copy() if trigger.get('serverScript'): trigger['body'] = str(trigger.pop('serverScript', '')) elif trigger.get('body'): trigger['body'] = str(trigger['body']) path = base.GetPathFromLink(collection_link, 'triggers') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) return collection_id, path, trigger def ReadTrigger(self, trigger_link, options=None): """Reads a trigger. :param str trigger_link: The link to the trigger. :param dict options: The request options for the request. :return: The read Trigger. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(trigger_link) trigger_id = base.GetResourceIdOrFullNameFromLink(trigger_link) return self.Read(path, 'triggers', trigger_id, None, options) def ReadUserDefinedFunctions(self, collection_link, options=None): """Reads all user defined functions in a collection. :param str collection_link: The link to the document collection. :param dict options: The request options for the request. :return: Query Iterable of UDFs. :rtype: query_iterable.QueryIterable """ if options is None: options = {} return self.QueryUserDefinedFunctions(collection_link, None, options) def QueryUserDefinedFunctions(self, collection_link, query, options=None): """Queries user defined functions in a collection. :param str collection_link: The link to the collection. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of UDFs. :rtype: query_iterable.QueryIterable """ if options is None: options = {} path = base.GetPathFromLink(collection_link, 'udfs') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) def fetch_fn(options): return self.__QueryFeed(path, 'udfs', collection_id, lambda r: r['UserDefinedFunctions'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def CreateUserDefinedFunction(self, collection_link, udf, options=None): """Creates a user defined function in a collection. :param str collection_link: The link to the collection. :param str udf: :param dict options: The request options for the request. :return: The created UDF. :rtype: dict """ if options is None: options = {} collection_id, path, udf = self._GetContainerIdWithPathForUDF(collection_link, udf) return self.Create(udf, path, 'udfs', collection_id, None, options) def UpsertUserDefinedFunction(self, collection_link, udf, options=None): """Upserts a user defined function in a collection. :param str collection_link: The link to the collection. :param str udf: :param dict options: The request options for the request. :return: The upserted UDF. :rtype: dict """ if options is None: options = {} collection_id, path, udf = self._GetContainerIdWithPathForUDF(collection_link, udf) return self.Upsert(udf, path, 'udfs', collection_id, None, options) def _GetContainerIdWithPathForUDF(self, collection_link, udf): CosmosClient.__ValidateResource(udf) udf = udf.copy() if udf.get('serverScript'): udf['body'] = str(udf.pop('serverScript', '')) elif udf.get('body'): udf['body'] = str(udf['body']) path = base.GetPathFromLink(collection_link, 'udfs') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) return collection_id, path, udf def ReadUserDefinedFunction(self, udf_link, options=None): """Reads a user defined function. :param str udf_link: The link to the user defined function. :param dict options: The request options for the request. :return: The read UDF. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(udf_link) udf_id = base.GetResourceIdOrFullNameFromLink(udf_link) return self.Read(path, 'udfs', udf_id, None, options) def ReadStoredProcedures(self, collection_link, options=None): """Reads all store procedures in a collection. :param str collection_link: The link to the document collection. :param dict options: The request options for the request. :return: Query Iterable of Stored Procedures. :rtype: query_iterable.QueryIterable """ if options is None: options = {} return self.QueryStoredProcedures(collection_link, None, options) def QueryStoredProcedures(self, collection_link, query, options=None): """Queries stored procedures in a collection. :param str collection_link: The link to the document collection. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of Stored Procedures. :rtype: query_iterable.QueryIterable """ if options is None: options = {} path = base.GetPathFromLink(collection_link, 'sprocs') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) def fetch_fn(options): return self.__QueryFeed(path, 'sprocs', collection_id, lambda r: r['StoredProcedures'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def CreateStoredProcedure(self, collection_link, sproc, options=None): """Creates a stored procedure in a collection. :param str collection_link: The link to the document collection. :param str sproc: :param dict options: The request options for the request. :return: The created Stored Procedure. :rtype: dict """ if options is None: options = {} collection_id, path, sproc = self._GetContainerIdWithPathForSproc(collection_link, sproc) return self.Create(sproc, path, 'sprocs', collection_id, None, options) def UpsertStoredProcedure(self, collection_link, sproc, options=None): """Upserts a stored procedure in a collection. :param str collection_link: The link to the document collection. :param str sproc: :param dict options: The request options for the request. :return: The upserted Stored Procedure. :rtype: dict """ if options is None: options = {} collection_id, path, sproc = self._GetContainerIdWithPathForSproc(collection_link, sproc) return self.Upsert(sproc, path, 'sprocs', collection_id, None, options) def _GetContainerIdWithPathForSproc(self, collection_link, sproc): CosmosClient.__ValidateResource(sproc) sproc = sproc.copy() if sproc.get('serverScript'): sproc['body'] = str(sproc.pop('serverScript', '')) elif sproc.get('body'): sproc['body'] = str(sproc['body']) path = base.GetPathFromLink(collection_link, 'sprocs') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) return collection_id, path, sproc def ReadStoredProcedure(self, sproc_link, options=None): """Reads a stored procedure. :param str sproc_link: The link to the stored procedure. :param dict options: The request options for the request. :return: The read Stored Procedure. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(sproc_link) sproc_id = base.GetResourceIdOrFullNameFromLink(sproc_link) return self.Read(path, 'sprocs', sproc_id, None, options) def ReadConflicts(self, collection_link, feed_options=None): """Reads conflicts. :param str collection_link: The link to the document collection. :param dict feed_options: :return: Query Iterable of Conflicts. :rtype: query_iterable.QueryIterable """ if feed_options is None: feed_options = {} return self.QueryConflicts(collection_link, None, feed_options) def QueryConflicts(self, collection_link, query, options=None): """Queries conflicts in a collection. :param str collection_link: The link to the document collection. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of Conflicts. :rtype: query_iterable.QueryIterable """ if options is None: options = {} path = base.GetPathFromLink(collection_link, 'conflicts') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) def fetch_fn(options): return self.__QueryFeed(path, 'conflicts', collection_id, lambda r: r['Conflicts'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def ReadConflict(self, conflict_link, options=None): """Reads a conflict. :param str conflict_link: The link to the conflict. :param dict options: :return: The read Conflict. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(conflict_link) conflict_id = base.GetResourceIdOrFullNameFromLink(conflict_link) return self.Read(path, 'conflicts', conflict_id, None, options) def DeleteContainer(self, collection_link, options=None): """Deletes a collection. :param str collection_link: The link to the document collection. :param dict options: The request options for the request. :return: The deleted Collection. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(collection_link) collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) return self.DeleteResource(path, 'colls', collection_id, None, options) def ReplaceItem(self, document_link, new_document, options=None): """Replaces a document and returns it. :param str document_link: The link to the document. :param dict new_document: :param dict options: The request options for the request. :return: The new Document. :rtype: dict """ CosmosClient.__ValidateResource(new_document) path = base.GetPathFromLink(document_link) document_id = base.GetResourceIdOrFullNameFromLink(document_link) # Python's default arguments are evaluated once when the function is defined, not each time the function is called (like it is in say, Ruby). # This means that if you use a mutable default argument and mutate it, you will and have mutated that object for all future calls to the function as well. # So, using a non-mutable deafult in this case(None) and assigning an empty dict(mutable) inside the function so that it remains local # For more details on this gotcha, please refer http://docs.python-guide.org/en/latest/writing/gotchas/ if options is None: options = {} # Extract the document collection link and add the partition key to options collection_link = base.GetItemContainerLink(document_link) options = self._AddPartitionKey(collection_link, new_document, options) return self.Replace(new_document, path, 'docs', document_id, None, options) def DeleteItem(self, document_link, options=None): """Deletes a document. :param str document_link: The link to the document. :param dict options: The request options for the request. :return: The deleted Document. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(document_link) document_id = base.GetResourceIdOrFullNameFromLink(document_link) return self.DeleteResource(path, 'docs', document_id, None, options) def CreateAttachment(self, document_link, attachment, options=None): """Creates an attachment in a document. :param str document_link: The link to the document. :param dict attachment: The Azure Cosmos attachment to create. :param dict options: The request options for the request. :return: The created Attachment. :rtype: dict """ if options is None: options = {} document_id, path = self._GetItemIdWithPathForAttachment(attachment, document_link) return self.Create(attachment, path, 'attachments', document_id, None, options) def UpsertAttachment(self, document_link, attachment, options=None): """Upserts an attachment in a document. :param str document_link: The link to the document. :param dict attachment: The Azure Cosmos attachment to upsert. :param dict options: The request options for the request. :return: The upserted Attachment. :rtype: dict """ if options is None: options = {} document_id, path = self._GetItemIdWithPathForAttachment(attachment, document_link) return self.Upsert(attachment, path, 'attachments', document_id, None, options) def _GetItemIdWithPathForAttachment(self, attachment, document_link): CosmosClient.__ValidateResource(attachment) path = base.GetPathFromLink(document_link, 'attachments') document_id = base.GetResourceIdOrFullNameFromLink(document_link) return document_id, path def CreateAttachmentAndUploadMedia(self, document_link, readable_stream, options=None): """Creates an attachment and upload media. :param str document_link: The link to the document. :param (file-like stream object) readable_stream: :param dict options: The request options for the request. :return: The created Attachment. :rtype: dict """ if options is None: options = {} document_id, initial_headers, path = self._GetItemIdWithPathForAttachmentMedia(document_link, options) return self.Create(readable_stream, path, 'attachments', document_id, initial_headers, options) def UpsertAttachmentAndUploadMedia(self, document_link, readable_stream, options=None): """Upserts an attachment and upload media. :param str document_link: The link to the document. :param (file-like stream object) readable_stream: :param dict options: The request options for the request. :return: The upserted Attachment. :rtype: dict """ if options is None: options = {} document_id, initial_headers, path = self._GetItemIdWithPathForAttachmentMedia(document_link, options) return self.Upsert(readable_stream, path, 'attachments', document_id, initial_headers, options) def _GetItemIdWithPathForAttachmentMedia(self, document_link, options): initial_headers = dict(self.default_headers) # Add required headers slug and content-type. if options.get('slug'): initial_headers[http_constants.HttpHeaders.Slug] = options['slug'] if options.get('contentType'): initial_headers[http_constants.HttpHeaders.ContentType] = ( options['contentType']) else: initial_headers[http_constants.HttpHeaders.ContentType] = ( runtime_constants.MediaTypes.OctetStream) path = base.GetPathFromLink(document_link, 'attachments') document_id = base.GetResourceIdOrFullNameFromLink(document_link) return document_id, initial_headers, path def ReadAttachment(self, attachment_link, options=None): """Reads an attachment. :param str attachment_link: The link to the attachment. :param dict options: The request options for the request. :return: The read Attachment. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(attachment_link) attachment_id = base.GetResourceIdOrFullNameFromLink(attachment_link) return self.Read(path, 'attachments', attachment_id, None, options) def ReadAttachments(self, document_link, options=None): """Reads all attachments in a document. :param str document_link: The link to the document. :param dict options: The request options for the request. :return: Query Iterable of Attachments. :rtype: query_iterable.QueryIterable """ if options is None: options = {} return self.QueryAttachments(document_link, None, options) def QueryAttachments(self, document_link, query, options=None): """Queries attachments in a document. :param str document_link: The link to the document. :param (str or dict) query: :param dict options: The request options for the request. :return: Query Iterable of Attachments. :rtype: query_iterable.QueryIterable """ if options is None: options = {} path = base.GetPathFromLink(document_link, 'attachments') document_id = base.GetResourceIdOrFullNameFromLink(document_link) def fetch_fn(options): return self.__QueryFeed(path, 'attachments', document_id, lambda r: r['Attachments'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def ReadMedia(self, media_link): """Reads a media. When self.connection_policy.MediaReadMode == documents.MediaReadMode.Streamed, returns a file-like stream object; otherwise, returns a str. :param str media_link: The link to the media. :return: The read Media. :rtype: str or file-like stream object """ default_headers = self.default_headers path = base.GetPathFromLink(media_link) media_id = base.GetResourceIdOrFullNameFromLink(media_link) attachment_id = base.GetAttachmentIdFromMediaId(media_id) headers = base.GetHeaders(self, default_headers, 'get', path, attachment_id, 'media', {}) # ReadMedia will always use WriteEndpoint since it's not replicated in readable Geo regions request = request_object._RequestObject('media', documents._OperationType.Read) result, self.last_response_headers = self.__Get(path, request, headers) return result def UpdateMedia(self, media_link, readable_stream, options=None): """Updates a media and returns it. :param str media_link: The link to the media. :param (file-like stream object) readable_stream: :param dict options: The request options for the request. :return: The updated Media. :rtype: str or file-like stream object """ if options is None: options = {} initial_headers = dict(self.default_headers) # Add required headers slug and content-type in case the body is a stream if options.get('slug'): initial_headers[http_constants.HttpHeaders.Slug] = options['slug'] if options.get('contentType'): initial_headers[http_constants.HttpHeaders.ContentType] = ( options['contentType']) else: initial_headers[http_constants.HttpHeaders.ContentType] = ( runtime_constants.MediaTypes.OctetStream) path = base.GetPathFromLink(media_link) media_id = base.GetResourceIdOrFullNameFromLink(media_link) attachment_id = base.GetAttachmentIdFromMediaId(media_id) headers = base.GetHeaders(self, initial_headers, 'put', path, attachment_id, 'media', options) # UpdateMedia will use WriteEndpoint since it uses PUT operation request = request_object._RequestObject('media', documents._OperationType.Update) result, self.last_response_headers = self.__Put(path, request, readable_stream, headers) self._UpdateSessionIfRequired(headers, result, self.last_response_headers) return result def ReplaceAttachment(self, attachment_link, attachment, options=None): """Replaces an attachment and returns it. :param str attachment_link: The link to the attachment. :param dict attachment: :param dict options: The request options for the request. :return: The replaced Attachment :rtype: dict """ if options is None: options = {} CosmosClient.__ValidateResource(attachment) path = base.GetPathFromLink(attachment_link) attachment_id = base.GetResourceIdOrFullNameFromLink(attachment_link) return self.Replace(attachment, path, 'attachments', attachment_id, None, options) def DeleteAttachment(self, attachment_link, options=None): """Deletes an attachment. :param str attachment_link: The link to the attachment. :param dict options: The request options for the request. :return: The deleted Attachment. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(attachment_link) attachment_id = base.GetResourceIdOrFullNameFromLink(attachment_link) return self.DeleteResource(path, 'attachments', attachment_id, None, options) def ReplaceTrigger(self, trigger_link, trigger, options=None): """Replaces a trigger and returns it. :param str trigger_link: The link to the trigger. :param dict trigger: :param dict options: The request options for the request. :return: The replaced Trigger. :rtype: dict """ if options is None: options = {} CosmosClient.__ValidateResource(trigger) trigger = trigger.copy() if trigger.get('serverScript'): trigger['body'] = str(trigger['serverScript']) elif trigger.get('body'): trigger['body'] = str(trigger['body']) path = base.GetPathFromLink(trigger_link) trigger_id = base.GetResourceIdOrFullNameFromLink(trigger_link) return self.Replace(trigger, path, 'triggers', trigger_id, None, options) def DeleteTrigger(self, trigger_link, options=None): """Deletes a trigger. :param str trigger_link: The link to the trigger. :param dict options: The request options for the request. :return: The deleted Trigger. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(trigger_link) trigger_id = base.GetResourceIdOrFullNameFromLink(trigger_link) return self.DeleteResource(path, 'triggers', trigger_id, None, options) def ReplaceUserDefinedFunction(self, udf_link, udf, options=None): """Replaces a user defined function and returns it. :param str udf_link: The link to the user defined function. :param dict udf: :param dict options: The request options for the request. :return: The new UDF. :rtype: dict """ if options is None: options = {} CosmosClient.__ValidateResource(udf) udf = udf.copy() if udf.get('serverScript'): udf['body'] = str(udf['serverScript']) elif udf.get('body'): udf['body'] = str(udf['body']) path = base.GetPathFromLink(udf_link) udf_id = base.GetResourceIdOrFullNameFromLink(udf_link) return self.Replace(udf, path, 'udfs', udf_id, None, options) def DeleteUserDefinedFunction(self, udf_link, options=None): """Deletes a user defined function. :param str udf_link: The link to the user defined function. :param dict options: The request options for the request. :return: The deleted UDF. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(udf_link) udf_id = base.GetResourceIdOrFullNameFromLink(udf_link) return self.DeleteResource(path, 'udfs', udf_id, None, options) def ExecuteStoredProcedure(self, sproc_link, params, options=None): """Executes a store procedure. :param str sproc_link: The link to the stored procedure. :param dict params: List or None :param dict options: The request options for the request. :return: The Stored Procedure response. :rtype: dict """ if options is None: options = {} initial_headers = dict(self.default_headers) initial_headers.update({ http_constants.HttpHeaders.Accept: ( runtime_constants.MediaTypes.Json) }) if params and not type(params) is list: params = [params] path = base.GetPathFromLink(sproc_link) sproc_id = base.GetResourceIdOrFullNameFromLink(sproc_link) headers = base.GetHeaders(self, initial_headers, 'post', path, sproc_id, 'sprocs', options) # ExecuteStoredProcedure will use WriteEndpoint since it uses POST operation request = request_object._RequestObject('sprocs', documents._OperationType.ExecuteJavaScript) result, self.last_response_headers = self.__Post(path, request, params, headers) return result def ReplaceStoredProcedure(self, sproc_link, sproc, options=None): """Replaces a stored procedure and returns it. :param str sproc_link: The link to the stored procedure. :param dict sproc: :param dict options: The request options for the request. :return: The replaced Stored Procedure. :rtype: dict """ if options is None: options = {} CosmosClient.__ValidateResource(sproc) sproc = sproc.copy() if sproc.get('serverScript'): sproc['body'] = str(sproc['serverScript']) elif sproc.get('body'): sproc['body'] = str(sproc['body']) path = base.GetPathFromLink(sproc_link) sproc_id = base.GetResourceIdOrFullNameFromLink(sproc_link) return self.Replace(sproc, path, 'sprocs', sproc_id, None, options) def DeleteStoredProcedure(self, sproc_link, options=None): """Deletes a stored procedure. :param str sproc_link: The link to the stored procedure. :param dict options: The request options for the request. :return: The deleted Stored Procedure. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(sproc_link) sproc_id = base.GetResourceIdOrFullNameFromLink(sproc_link) return self.DeleteResource(path, 'sprocs', sproc_id, None, options) def DeleteConflict(self, conflict_link, options=None): """Deletes a conflict. :param str conflict_link: The link to the conflict. :param dict options: The request options for the request. :return: The deleted Conflict. :rtype: dict """ if options is None: options = {} path = base.GetPathFromLink(conflict_link) conflict_id = base.GetResourceIdOrFullNameFromLink(conflict_link) return self.DeleteResource(path, 'conflicts', conflict_id, None, options) def ReplaceOffer(self, offer_link, offer): """Replaces an offer and returns it. :param str offer_link: The link to the offer. :param dict offer: :return: The replaced Offer. :rtype: dict """ CosmosClient.__ValidateResource(offer) path = base.GetPathFromLink(offer_link) offer_id = base.GetResourceIdOrFullNameFromLink(offer_link) return self.Replace(offer, path, 'offers', offer_id, None, None) def ReadOffer(self, offer_link): """Reads an offer. :param str offer_link: The link to the offer. :return: The read Offer. :rtype: dict """ path = base.GetPathFromLink(offer_link) offer_id = base.GetResourceIdOrFullNameFromLink(offer_link) return self.Read(path, 'offers', offer_id, None, {}) def ReadOffers(self, options=None): """Reads all offers. :param dict options: The request options for the request :return: Query Iterable of Offers. :rtype: query_iterable.QueryIterable """ if options is None: options = {} return self.QueryOffers(None, options) def QueryOffers(self, query, options=None): """Query for all offers. :param (str or dict) query: :param dict options: The request options for the request :return: Query Iterable of Offers. :rtype: query_iterable.QueryIterable """ if options is None: options = {} def fetch_fn(options): return self.__QueryFeed('/offers', 'offers', '', lambda r: r['Offers'], lambda _, b: b, query, options), self.last_response_headers return query_iterable.QueryIterable(self, query, options, fetch_fn) def GetDatabaseAccount(self, url_connection=None): """Gets database account info. :return: The Database Account. :rtype: documents.DatabaseAccount """ if url_connection is None: url_connection = self.url_connection initial_headers = dict(self.default_headers) headers = base.GetHeaders(self, initial_headers, 'get', '', # path '', # id '', # type {}) request = request_object._RequestObject('databaseaccount', documents._OperationType.Read, url_connection) result, self.last_response_headers = self.__Get('', request, headers) database_account = documents.DatabaseAccount() database_account.DatabasesLink = '/dbs/' database_account.MediaLink = '/media/' if (http_constants.HttpHeaders.MaxMediaStorageUsageInMB in self.last_response_headers): database_account.MaxMediaStorageUsageInMB = ( self.last_response_headers[ http_constants.HttpHeaders.MaxMediaStorageUsageInMB]) if (http_constants.HttpHeaders.CurrentMediaStorageUsageInMB in self.last_response_headers): database_account.CurrentMediaStorageUsageInMB = ( self.last_response_headers[ http_constants.HttpHeaders.CurrentMediaStorageUsageInMB]) database_account.ConsistencyPolicy = result.get(constants._Constants.UserConsistencyPolicy) # WritableLocations and ReadableLocations fields will be available only for geo-replicated database accounts if constants._Constants.WritableLocations in result: database_account._WritableLocations = result[constants._Constants.WritableLocations] if constants._Constants.ReadableLocations in result: database_account._ReadableLocations = result[constants._Constants.ReadableLocations] if constants._Constants.EnableMultipleWritableLocations in result: database_account._EnableMultipleWritableLocations = result[constants._Constants.EnableMultipleWritableLocations] self._useMultipleWriteLocations = self.connection_policy.UseMultipleWriteLocations and database_account._EnableMultipleWritableLocations return database_account def Create(self, body, path, type, id, initial_headers, options=None): """Creates a Azure Cosmos resource and returns it. :param dict body: :param str path: :param str type: :param str id: :param dict initial_headers: :param dict options: The request options for the request. :return: The created Azure Cosmos resource. :rtype: dict """ if options is None: options = {} initial_headers = initial_headers or self.default_headers headers = base.GetHeaders(self, initial_headers, 'post', path, id, type, options) # Create will use WriteEndpoint since it uses POST operation request = request_object._RequestObject(type, documents._OperationType.Create) result, self.last_response_headers = self.__Post(path, request, body, headers) # update session for write request self._UpdateSessionIfRequired(headers, result, self.last_response_headers) return result def Upsert(self, body, path, type, id, initial_headers, options=None): """Upserts a Azure Cosmos resource and returns it. :param dict body: :param str path: :param str type: :param str id: :param dict initial_headers: :param dict options: The request options for the request. :return: The upserted Azure Cosmos resource. :rtype: dict """ if options is None: options = {} initial_headers = initial_headers or self.default_headers headers = base.GetHeaders(self, initial_headers, 'post', path, id, type, options) headers[http_constants.HttpHeaders.IsUpsert] = True # Upsert will use WriteEndpoint since it uses POST operation request = request_object._RequestObject(type, documents._OperationType.Upsert) result, self.last_response_headers = self.__Post(path, request, body, headers) # update session for write request self._UpdateSessionIfRequired(headers, result, self.last_response_headers) return result def Replace(self, resource, path, type, id, initial_headers, options=None): """Replaces a Azure Cosmos resource and returns it. :param dict resource: :param str path: :param str type: :param str id: :param dict initial_headers: :param dict options: The request options for the request. :return: The new Azure Cosmos resource. :rtype: dict """ if options is None: options = {} initial_headers = initial_headers or self.default_headers headers = base.GetHeaders(self, initial_headers, 'put', path, id, type, options) # Replace will use WriteEndpoint since it uses PUT operation request = request_object._RequestObject(type, documents._OperationType.Replace) result, self.last_response_headers = self.__Put(path, request, resource, headers) # update session for request mutates data on server side self._UpdateSessionIfRequired(headers, result, self.last_response_headers) return result def Read(self, path, type, id, initial_headers, options=None): """Reads a Azure Cosmos resource and returns it. :param str path: :param str type: :param str id: :param dict initial_headers: :param dict options: The request options for the request. :return: The upserted Azure Cosmos resource. :rtype: dict """ if options is None: options = {} initial_headers = initial_headers or self.default_headers headers = base.GetHeaders(self, initial_headers, 'get', path, id, type, options) # Read will use ReadEndpoint since it uses GET operation request = request_object._RequestObject(type, documents._OperationType.Read) result, self.last_response_headers = self.__Get(path, request, headers) return result def DeleteResource(self, path, type, id, initial_headers, options=None): """Deletes a Azure Cosmos resource and returns it. :param str path: :param str type: :param str id: :param dict initial_headers: :param dict options: The request options for the request. :return: The deleted Azure Cosmos resource. :rtype: dict """ if options is None: options = {} initial_headers = initial_headers or self.default_headers headers = base.GetHeaders(self, initial_headers, 'delete', path, id, type, options) # Delete will use WriteEndpoint since it uses DELETE operation request = request_object._RequestObject(type, documents._OperationType.Delete) result, self.last_response_headers = self.__Delete(path, request, headers) # update session for request mutates data on server side self._UpdateSessionIfRequired(headers, result, self.last_response_headers) return result def __Get(self, path, request, headers): """Azure Cosmos 'GET' http request. :params str url: :params str path: :params dict headers: :return: Tuple of (result, headers). :rtype: tuple of (dict, dict) """ return synchronized_request.SynchronizedRequest(self, request, self._global_endpoint_manager, self.connection_policy, self._requests_session, 'GET', path, None, None, headers) def __Post(self, path, request, body, headers): """Azure Cosmos 'POST' http request. :params str url: :params str path: :params (str, unicode, dict) body: :params dict headers: :return: Tuple of (result, headers). :rtype: tuple of (dict, dict) """ return synchronized_request.SynchronizedRequest(self, request, self._global_endpoint_manager, self.connection_policy, self._requests_session, 'POST', path, body, query_params=None, headers=headers) def __Put(self, path, request, body, headers): """Azure Cosmos 'PUT' http request. :params str url: :params str path: :params (str, unicode, dict) body: :params dict headers: :return: Tuple of (result, headers). :rtype: tuple of (dict, dict) """ return synchronized_request.SynchronizedRequest(self, request, self._global_endpoint_manager, self.connection_policy, self._requests_session, 'PUT', path, body, query_params=None, headers=headers) def __Delete(self, path, request, headers): """Azure Cosmos 'DELETE' http request. :params str url: :params str path: :params dict headers: :return: Tuple of (result, headers). :rtype: tuple of (dict, dict) """ return synchronized_request.SynchronizedRequest(self, request, self._global_endpoint_manager, self.connection_policy, self._requests_session, 'DELETE', path, request_data=None, query_params=None, headers=headers) def QueryFeed(self, path, collection_id, query, options, partition_key_range_id = None): """Query Feed for Document Collection resource. :param str path: Path to the document collection. :param str collection_id: Id of the document collection. :param (str or dict) query: :param dict options: The request options for the request. :param str partition_key_range_id: Partition key range id. :rtype: tuple """ return self.__QueryFeed(path, 'docs', collection_id, lambda r: r['Documents'], lambda _, b: b, query, options, partition_key_range_id), self.last_response_headers def __QueryFeed(self, path, type, id, result_fn, create_fn, query, options=None, partition_key_range_id=None): """Query for more than one Azure Cosmos resources. :param str path: :param str type: :param str id: :param function result_fn: :param function create_fn: :param (str or dict) query: :param dict options: The request options for the request. :param str partition_key_range_id: Specifies partition key range id. :rtype: list :raises SystemError: If the query compatibility mode is undefined. """ if options is None: options = {} if query: __GetBodiesFromQueryResult = result_fn else: def __GetBodiesFromQueryResult(result): if result is not None: return [create_fn(self, body) for body in result_fn(result)] else: # If there is no change feed, the result data is empty and result is None. # This case should be interpreted as an empty array. return [] initial_headers = self.default_headers.copy() # Copy to make sure that default_headers won't be changed. if query is None: # Query operations will use ReadEndpoint even though it uses GET(for feed requests) request = request_object._RequestObject(type, documents._OperationType.ReadFeed) headers = base.GetHeaders(self, initial_headers, 'get', path, id, type, options, partition_key_range_id) result, self.last_response_headers = self.__Get(path, request, headers) return __GetBodiesFromQueryResult(result) else: query = self.__CheckAndUnifyQueryFormat(query) initial_headers[http_constants.HttpHeaders.IsQuery] = 'true' if (self._query_compatibility_mode == CosmosClient._QueryCompatibilityMode.Default or self._query_compatibility_mode == CosmosClient._QueryCompatibilityMode.Query): initial_headers[http_constants.HttpHeaders.ContentType] = runtime_constants.MediaTypes.QueryJson elif self._query_compatibility_mode == CosmosClient._QueryCompatibilityMode.SqlQuery: initial_headers[http_constants.HttpHeaders.ContentType] = runtime_constants.MediaTypes.SQL else: raise SystemError('Unexpected query compatibility mode.') # Query operations will use ReadEndpoint even though it uses POST(for regular query operations) request = request_object._RequestObject(type, documents._OperationType.SqlQuery) headers = base.GetHeaders(self, initial_headers, 'post', path, id, type, options, partition_key_range_id) result, self.last_response_headers = self.__Post(path, request, query, headers) return __GetBodiesFromQueryResult(result) def __CheckAndUnifyQueryFormat(self, query_body): """Checks and unifies the format of the query body. :raises TypeError: If query_body is not of expected type (depending on the query compatibility mode). :raises ValueError: If query_body is a dict but doesn\'t have valid query text. :raises SystemError: If the query compatibility mode is undefined. :param (str or dict) query_body: :return: The formatted query body. :rtype: dict or string """ if (self._query_compatibility_mode == CosmosClient._QueryCompatibilityMode.Default or self._query_compatibility_mode == CosmosClient._QueryCompatibilityMode.Query): if not isinstance(query_body, dict) and not isinstance(query_body, six.string_types): raise TypeError('query body must be a dict or string.') if isinstance(query_body, dict) and not query_body.get('query'): raise ValueError('query body must have valid query text with key "query".') if isinstance(query_body, six.string_types): return {'query': query_body} elif (self._query_compatibility_mode == CosmosClient._QueryCompatibilityMode.SqlQuery and not isinstance(query_body, six.string_types)): raise TypeError('query body must be a string.') else: raise SystemError('Unexpected query compatibility mode.') return query_body @staticmethod def __ValidateResource(resource): id = resource.get('id') if id: if id.find('/') != -1 or id.find('\\') != -1 or id.find('?') != -1 or id.find('#') != -1: raise ValueError('Id contains illegal chars.') if id[-1] == ' ': raise ValueError('Id ends with a space.') # Adds the partition key to options def _AddPartitionKey(self, collection_link, document, options): collection_link = base.TrimBeginningAndEndingSlashes(collection_link) #TODO: Refresh the cache if partition is extracted automatically and we get a 400.1001 # If the document collection link is present in the cache, then use the cached partitionkey definition if collection_link in self.partition_key_definition_cache: partitionKeyDefinition = self.partition_key_definition_cache.get(collection_link) # Else read the collection from backend and add it to the cache else: collection = self.ReadContainer(collection_link) partitionKeyDefinition = collection.get('partitionKey') self.partition_key_definition_cache[collection_link] = partitionKeyDefinition # If the collection doesn't have a partition key definition, skip it as it's a legacy collection if partitionKeyDefinition: # If the user has passed in the partitionKey in options use that elase extract it from the document if('partitionKey' not in options): partitionKeyValue = self._ExtractPartitionKey(partitionKeyDefinition, document) options['partitionKey'] = partitionKeyValue return options # Extracts the partition key from the document using the partitionKey definition def _ExtractPartitionKey(self, partitionKeyDefinition, document): # Parses the paths into a list of token each representing a property partition_key_parts = base.ParsePaths(partitionKeyDefinition.get('paths')) # Navigates the document to retrieve the partitionKey specified in the paths return self._RetrievePartitionKey(partition_key_parts, document) # Navigates the document to retrieve the partitionKey specified in the partition key parts def _RetrievePartitionKey(self, partition_key_parts, document): expected_matchCount = len(partition_key_parts) matchCount = 0 partitionKey = document for part in partition_key_parts: # At any point if we don't find the value of a sub-property in the document, we return as Undefined if part not in partitionKey: return documents.Undefined else: partitionKey = partitionKey.get(part) matchCount += 1 # Once we reach the "leaf" value(not a dict), we break from loop if not isinstance(partitionKey, dict): break # Match the count of hops we did to get the partitionKey with the length of partition key parts and validate that it's not a dict at that level if ((matchCount != expected_matchCount) or isinstance(partitionKey, dict)): return documents.Undefined return partitionKey def _UpdateSessionIfRequired(self, request_headers, response_result, response_headers): """ Updates session if necessary. :param dict response_result: :param dict response_headers: :param dict response_headers :return: None, but updates the client session if necessary. """ '''if this request was made with consistency level as session, then update the session''' if response_result is None or response_headers is None: return is_session_consistency = False if http_constants.HttpHeaders.ConsistencyLevel in request_headers: if documents.ConsistencyLevel.Session == request_headers[http_constants.HttpHeaders.ConsistencyLevel]: is_session_consistency = True if is_session_consistency: # update session self.session.update_session(response_result, response_headers) azure-cosmos-python-3.1.1/azure/cosmos/default_retry_policy.py000066400000000000000000000061571352206500100247200ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2017 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for connection reset retry policy implementation in the Azure Cosmos database service. """ import azure.cosmos.http_constants as http_constants class _DefaultRetryPolicy(object): error_codes = http_constants._ErrorCodes CONNECTION_ERROR_CODES = [ error_codes.WindowsInterruptedFunctionCall, error_codes.WindowsFileHandleNotValid, error_codes.WindowsPermissionDenied, error_codes.WindowsBadAddress, error_codes.WindowsInvalidArgumnet, error_codes.WindowsResourceTemporarilyUnavailable, error_codes.WindowsOperationNowInProgress, error_codes.WindowsAddressAlreadyInUse, error_codes.WindowsConnectionResetByPeer, error_codes.WindowsCannotSendAfterSocketShutdown, error_codes.WindowsConnectionTimedOut, error_codes.WindowsConnectionRefused, error_codes.WindowsNameTooLong, error_codes.WindowsHostIsDown, error_codes.WindowsNoRouteTohost, error_codes.LinuxConnectionReset ] def __init__(self, *args): self._max_retry_attempt_count = 10 self.current_retry_attempt_count = 0 self.retry_after_in_milliseconds = 1000 self.args = args def needsRetry(self, error_code): if error_code in _DefaultRetryPolicy.CONNECTION_ERROR_CODES: if (len(self.args) > 0): if (self.args[4]['method'] == 'GET') or (http_constants.HttpHeaders.IsQuery in self.args[4]['headers']): return True return False return True def ShouldRetry(self, exception): """Returns true if should retry based on the passed-in exception. :param (errors.HTTPFailure instance) exception: :rtype: boolean """ if (self.current_retry_attempt_count < self._max_retry_attempt_count) and self.needsRetry(exception.status_code): self.current_retry_attempt_count += 1 return True return False azure-cosmos-python-3.1.1/azure/cosmos/documents.py000066400000000000000000000361001352206500100224600ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """AzureDocument classes and enums for the Azure Cosmos database service. """ import azure.cosmos.retry_options as retry_options class DatabaseAccount(object): """Database account. A DatabaseAccount is the container for databases. :ivar str DatabaseLink: The self-link for Databases in the databaseAccount. :ivar str MediaLink: The self-link for Media in the databaseAccount. :ivar int MaxMediaStorageUsageInMB: Attachment content (media) storage quota in MBs (Retrieved from gateway). :ivar int CurrentMediaStorageUsageInMB: Current attachment content (media) usage in MBs (Retrieved from gateway). Value is returned from cached information updated periodically and is not guaranteed to be real time. :ivar dict ConsistencyPolicy: UserConsistencyPolicy settings. :ivar dict ConsistencyPolicy['defaultConsistencyLevel']: The default consistency level. :ivar int ConsistencyPolicy['maxStalenessPrefix']: In bounded staleness consistency, the maximum allowed staleness in terms difference in sequence numbers (aka version). :ivar int ConsistencyPolicy['maxStalenessIntervalInSeconds']: In bounded staleness consistency, the maximum allowed staleness in terms time interval. :ivar boolean EnableMultipleWritableLocations: Flag on the azure Cosmos account that indicates if writes can take place in multiple locations. """ def __init__(self): self.DatabasesLink = '' self.MediaLink = '' self.MaxMediaStorageUsageInMB = 0 self.CurrentMediaStorageUsageInMB = 0 self.ConsumedDocumentStorageInMB = 0 self.ReservedDocumentStorageInMB = 0 self.ProvisionedDocumentStorageInMB = 0 self.ConsistencyPolicy = None self._WritableLocations = [] self._ReadableLocations = [] self._EnableMultipleWritableLocations = False @property def WritableLocations(self): """Gets the list of writable locations for a geo-replicated database account. """ return self._WritableLocations @property def ReadableLocations(self): """Gets the list of readable locations for a geo-replicated database account. """ return self._ReadableLocations class ConsistencyLevel(object): """Represents the consistency levels supported for Azure Cosmos client operations. The requested ConsistencyLevel must match or be weaker than that provisioned for the database account. Consistency levels. Consistency levels by order of strength are Strong, BoundedStaleness, Session, ConsistentPrefix and Eventual. :ivar str ConsistencyLevel.Strong: Strong Consistency guarantees that read operations always return the value that was last written. :ivar str ConsistencyLevel.BoundedStaleness: Bounded Staleness guarantees that reads are not too out-of-date. This can be configured based on number of operations (MaxStalenessPrefix) or time (MaxStalenessIntervalInSeconds). :ivar str ConsistencyLevel.Session: Session Consistency guarantees monotonic reads (you never read old data, then new, then old again), monotonic writes (writes are ordered) and read your writes (your writes are immediately visible to your reads) within any single session. :ivar str ConsistencyLevel.Eventual: Eventual Consistency guarantees that reads will return a subset of writes. All writes will be eventually be available for reads. :ivar str ConsistencyLevel.ConsistentPrefix: ConsistentPrefix Consistency guarantees that reads will return some prefix of all writes with no gaps. All writes will be eventually be available for reads. """ Strong = 'Strong' BoundedStaleness = 'BoundedStaleness' Session = 'Session' Eventual = 'Eventual' ConsistentPrefix = 'ConsistentPrefix' class IndexingMode(object): """Specifies the supported indexing modes. :ivar str Consistent: Index is updated synchronously with a create or update operation. With consistent indexing, query behavior is the same as the default consistency level for the collection. The index is always kept up to date with the data. :ivar str Lazy: Index is updated asynchronously with respect to a create or update operation. With lazy indexing, queries are eventually consistent. The index is updated when the collection is idle. :ivar str NoIndex: No index is provided. Setting IndexingMode to "None" drops the index. Use this if you don't want to maintain the index for a document collection, to save the storage cost or improve the write throughput. Your queries will degenerate to scans of the entire collection. """ Consistent = 'consistent' Lazy = 'lazy' NoIndex = 'none' class IndexKind(object): """Specifies the index kind of index specs. :ivar str IndexKind.Hash: The index entries are hashed to serve point look up queries. Can be used to serve queries like: SELECT * FROM docs d WHERE d.prop = 5 :ivar str IndexKind.Range: The index entries are ordered. Range indexes are optimized for inequality predicate queries with efficient range scans. Can be used to serve queries like: SELECT * FROM docs d WHERE d.prop > 5 """ Hash = 'Hash' Range = 'Range' class PartitionKind(object): """Specifies the kind of partitioning to be applied. :ivar str PartitionKind.Hash: The partition key definition path is hashed. """ Hash = 'Hash' class DataType(object): """Specifies the data type of index specs. :ivar str Number: Represents a numeric data type. :ivar str String: Represents a string data type. :ivar str Point: Represents a point data type. :ivar str LineString: Represents a line string data type. :ivar str Polygon: Represents a polygon data type. :ivar str MultiPolygon: Represents a multi-polygon data type. """ Number = 'Number' String = 'String' Point = 'Point' LineString = 'LineString' Polygon = 'Polygon' MultiPolygon = 'MultiPolygon' class IndexingDirective(object): """Specifies whether or not the resource is to be indexed. :ivar int Default: Use any pre-defined/pre-configured defaults. :ivar int Exclude: Index the resource. :ivar int Include: Do not index the resource. """ Default = 0 Exclude = 1 Include = 2 class ConnectionMode(object): """Represents the connection mode to be used by the client. :ivar int Gateway: Use the Azure Cosmos gateway to route all requests. The gateway proxies requests to the right data partition. """ Gateway = 0 class MediaReadMode(object): """Represents the mode for use with downloading attachment content (aka media). :ivar str Buffered: Content is buffered at the client and not directly streamed from the content store. Use Buffered to reduce the time taken to read and write media files. :ivar str Streamed: Content is directly streamed from the content store without any buffering at the client. Use Streamed to reduce the client memory overhead of reading and writing media files. """ Buffered = 'Buffered' Streamed = 'Streamed' class PermissionMode(object): """Enumeration specifying applicability of permission. :ivar str PermissionMode.NoneMode: None. :ivar str PermissionMode.Read: Permission applicable for read operations only. :ivar str PermissionMode.All: Permission applicable for all operations. """ NoneMode = 'none' # None is python's key word. Read = 'read' All = 'all' class TriggerType(object): """Specifies the type of the trigger. :ivar str TriggerType.Pre: Trigger should be executed before the associated operation(s). :ivar str TriggerType.Post: Trigger should be executed after the associated operation(s). """ Pre = 'pre' Post = 'post' class TriggerOperation(object): """Specifies the operations on which a trigger should be executed. :ivar str TriggerOperation.All: All operations. :ivar str TriggerOperation.Create: Create operations only. :ivar str TriggerOperation.Update: Update operations only. :ivar str TriggerOperation.Delete: Delete operations only. :ivar str TriggerOperation.Replace: Replace operations only. """ All = 'all' Create = 'create' Update = 'update' Delete = 'delete' Replace = 'replace' class SSLConfiguration(object): """Configurations for SSL connections. Please refer to http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification for more detail. :ivar str SSLKeyFIle: The path of the key file for ssl connection. :ivar str SSLCertFile: The path of the cert file for ssl connection. :ivar str SSLCaCerts: The path of the CA_BUNDLE file with certificates of trusted CAs. """ def __init__(self): self.SSLKeyFile = None self.SSLCertFile = None self.SSLCaCerts = None class ProxyConfiguration(object): """Configurations for proxy. :ivar str Host: The host address of the proxy. :ivar int Port: The port number of the proxy. """ def __init__(self): self.Host = None self.Port = None class ConnectionPolicy(object): """Represents the Connection policy assocated with a CosmosClient. :ivar int RequestTimeout: Gets or sets the request timeout (time to wait for response from network peer). :ivar int MediaRequestTimeout: Gets or sets Time to wait for response from network peer for attachment content (aka media) operations. :ivar documents.ConnectionMode ConnectionMode: Gets or sets the connection mode used in the client. Currently only Gateway is supported. :ivar MediaReadMode.Buffered MediaReadMode: Gets or sets the attachment content (aka media) download mode. :ivar documents.SSLConfiguration SSLConfiguration: Gets or sets the SSL configuration. :ivar documents.ProxyConfiguration ProxyConfiguration: Gets or sets the proxy configuration. :ivar boolean EnableEndpointDiscovery: Gets or sets endpoint discovery flag for geo-replicated database accounts. When EnableEndpointDiscovery is true, the client will automatically discover the current write and read locations and direct the requests to the correct location taking into consideration of the user's preference(if provided) as PreferredLocations. :ivar list PreferredLocations: Gets or sets the preferred locations for geo-replicated database accounts. When EnableEndpointDiscovery is true and PreferredLocations is non-empty, the client will use this list to evaluate the final location, taking into consideration the order specified in PreferredLocations list. The locations in this list are specified as the names of the azure Cosmos locations like, 'West US', 'East US', 'Central India' and so on. :ivar RetryOptions RetryOptions: Gets or sets the retry options to be applied to all requests when retrying. :ivar boolean DisableSSLVerification: Flag to disable SSL verification for the requests. SSL verification is enabled by default. Don't set this when targeting production endpoints. This is intended to be used only when targeting emulator endpoint to avoid failing your requests with SSL related error. :ivar boolean UseMultipleWriteLocations: Flag to enable writes on any locations (regions) for geo-replicated database accounts in the azure Cosmos service. """ __defaultRequestTimeout = 60000 # milliseconds # defaultMediaRequestTimeout is based upon the blob client timeout and the # retry policy. __defaultMediaRequestTimeout = 300000 # milliseconds def __init__(self): self.RequestTimeout = self.__defaultRequestTimeout self.MediaRequestTimeout = self.__defaultMediaRequestTimeout self.ConnectionMode = ConnectionMode.Gateway self.MediaReadMode = MediaReadMode.Buffered self.SSLConfiguration = None self.ProxyConfiguration = None self.EnableEndpointDiscovery = True self.PreferredLocations = [] self.RetryOptions = retry_options.RetryOptions() self.DisableSSLVerification = False self.UseMultipleWriteLocations = False class Undefined(object): """Represents undefined value for partitionKey when it's mising. """ class _OperationType(object): """Represents the type of the operation """ Create = 'Create' Delete = 'Delete' ExecuteJavaScript = 'ExecuteJavaScript' Head = 'Head' HeadFeed = 'HeadFeed' Query = 'Query' Read = 'Read' ReadFeed = 'ReadFeed' Recreate = 'Recreate' Replace = 'Replace' SqlQuery = 'SqlQuery' Update = 'Update' Upsert = 'Upsert' @staticmethod def IsWriteOperation(operationType): return (operationType == _OperationType.Create or operationType == _OperationType.Delete or operationType == _OperationType.Recreate or operationType == _OperationType.ExecuteJavaScript or operationType == _OperationType.Replace or operationType == _OperationType.Upsert or operationType == _OperationType.Update) @staticmethod def IsReadOnlyOperation(operationType): return (operationType == _OperationType.Read or operationType == _OperationType.ReadFeed or operationType == _OperationType.Head or operationType == _OperationType.HeadFeed or operationType == _OperationType.Query or operationType == _OperationType.SqlQuery) azure-cosmos-python-3.1.1/azure/cosmos/endpoint_discovery_retry_policy.py000066400000000000000000000110521352206500100271710ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for endpoint discovery retry policy implementation in the Azure Cosmos database service. """ import logging from azure.cosmos.documents import _OperationType logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) log_formatter = logging.Formatter('%(levelname)s:%(message)s') log_handler = logging.StreamHandler() log_handler.setFormatter(log_formatter) logger.addHandler(log_handler) class _EndpointDiscoveryRetryPolicy(object): """The endpoint discovery retry policy class used for geo-replicated database accounts to handle the write forbidden exceptions due to writable/readable location changes (say, after a failover). """ Max_retry_attempt_count = 120 Retry_after_in_milliseconds = 1000 def __init__(self, connection_policy, global_endpoint_manager, *args): self.global_endpoint_manager = global_endpoint_manager self._max_retry_attempt_count = _EndpointDiscoveryRetryPolicy.Max_retry_attempt_count self.failover_retry_count = 0 self.retry_after_in_milliseconds = _EndpointDiscoveryRetryPolicy.Retry_after_in_milliseconds self.connection_policy = connection_policy self.request = args[0] if args else None #clear previous location-based routing directive if (self.request): self.request.clear_route_to_location() # Resolve the endpoint for the request and pin the resolution to the resolved endpoint # This enables marking the endpoint unavailability on endpoint failover/unreachability self.location_endpoint = self.global_endpoint_manager.resolve_service_endpoint(self.request) self.request.route_to_location(self.location_endpoint) def ShouldRetry(self, exception): """Returns true if should retry based on the passed-in exception. :param (errors.HTTPFailure instance) exception: :rtype: boolean """ if not self.connection_policy.EnableEndpointDiscovery: return False if self.failover_retry_count >= self.Max_retry_attempt_count: return False self.failover_retry_count += 1 if self.location_endpoint: if _OperationType.IsReadOnlyOperation(self.request.operation_type): #Mark current read endpoint as unavailable self.global_endpoint_manager.mark_endpoint_unavailable_for_read(self.location_endpoint) else: self.global_endpoint_manager.mark_endpoint_unavailable_for_write(self.location_endpoint) # set the refresh_needed flag to ensure that endpoint list is # refreshed with new writable and readable locations self.global_endpoint_manager.refresh_needed = True # clear previous location-based routing directive self.request.clear_route_to_location() # set location-based routing directive based on retry count # simulating single master writes by ensuring usePreferredLocations # is set to false self.request.route_to_location_with_preferred_location_flag(self.failover_retry_count, False) # Resolve the endpoint for the request and pin the resolution to the resolved endpoint # This enables marking the endpoint unavailability on endpoint failover/unreachability self.location_endpoint = self.global_endpoint_manager.resolve_service_endpoint(self.request) self.request.route_to_location(self.location_endpoint) return True azure-cosmos-python-3.1.1/azure/cosmos/errors.py000066400000000000000000000045651352206500100220050ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """PyCosmos Exceptions in the Azure Cosmos database service. """ import azure.cosmos.http_constants as http_constants class CosmosError(Exception): """Base class for all Azure Cosmos errors. """ class HTTPFailure(CosmosError): """Raised when a HTTP request to the Azure Cosmos has failed. """ def __init__(self, status_code, message='', headers=None): """ :param int status_code: :param str message: """ if headers is None: headers = {} self.status_code = status_code self.headers = headers self.sub_status = None self._http_error_message = message if http_constants.HttpHeaders.SubStatus in self.headers: self.sub_status = int(self.headers[http_constants.HttpHeaders.SubStatus]) CosmosError.__init__(self, 'Status code: %d Sub-status: %d\n%s' % (self.status_code, self.sub_status, message)) else: CosmosError.__init__(self, 'Status code: %d\n%s' % (self.status_code, message)) class JSONParseFailure(CosmosError): """Raised when fails to parse JSON message. """ class UnexpectedDataType(CosmosError): """Raised when unexpected data type is provided as parameter. """azure-cosmos-python-3.1.1/azure/cosmos/execution_context/000077500000000000000000000000001352206500100236545ustar00rootroot00000000000000azure-cosmos-python-3.1.1/azure/cosmos/execution_context/__init__.py000066400000000000000000000021171352206500100257660ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE.azure-cosmos-python-3.1.1/azure/cosmos/execution_context/aggregators.py000066400000000000000000000063421352206500100265400ustar00rootroot00000000000000# The MIT License (MIT) # Copyright (c) 2017 Microsoft Corporation # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. """Internal class for aggregation queries implementation in the Azure Cosmos database service. """ from abc import abstractmethod, ABCMeta from azure.cosmos.execution_context.document_producer import _OrderByHelper class _Aggregator(object): __metaclass__ = ABCMeta @abstractmethod def aggregate(self, other): pass @abstractmethod def get_result(self): pass class _AverageAggregator(_Aggregator): def __init__(self): self.sum = None self.count = None def aggregate(self, other): if other is None or not 'sum' in other: return if self.sum is None: self.sum = 0.0 self.count = 0 self.sum += other['sum'] self.count += other['count'] def get_result(self): if self.sum is None or self.count is None or self.count <= 0: return None return self.sum / self.count class _CountAggregator(_Aggregator): def __init__(self): self.count = 0 def aggregate(self, other): self.count += other def get_result(self): return self.count class _MinAggregator(_Aggregator): def __init__(self): self.value = None def aggregate(self, other): if self.value is None: self.value = other else: if (_OrderByHelper.compare({'item': other}, {'item': self.value}) < 0): self.value = other def get_result(self): return self.value class _MaxAggregator(_Aggregator): def __init__(self): self.value = None def aggregate(self, other): if self.value is None: self.value = other else: if (_OrderByHelper.compare({'item': other}, {'item': self.value}) > 0): self.value = other def get_result(self): return self.value class _SumAggregator(_Aggregator): def __init__(self): self.sum = None def aggregate(self, other): if other is None: return if self.sum is None: self.sum = other else: self.sum += other def get_result(self): return self.sum azure-cosmos-python-3.1.1/azure/cosmos/execution_context/base_execution_context.py000066400000000000000000000237521352206500100310000ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for query execution context implementation in the Azure Cosmos database service. """ from collections import deque import azure.cosmos.retry_utility as retry_utility import azure.cosmos.http_constants as http_constants import azure.cosmos.base as base class _QueryExecutionContextBase(object): """ This is the abstract base execution context class. """ def __init__(self, client, options): """ Constructor :param CosmosClient client: :param dict options: The request options for the request. """ self._client = client self._options = options self._is_change_feed = 'changeFeed' in options and options['changeFeed'] is True self._continuation = None if 'continuation' in options and self._is_change_feed: self._continuation = options['continuation'] self._has_started = False self._has_finished = False self._buffer = deque() def _has_more_pages(self): return not self._has_started or self._continuation def fetch_next_block(self): """Returns a block of results with respecting retry policy. This method only exists for backward compatibility reasons. (Because QueryIterable has exposed fetch_next_block api). :return: List of results. :rtype: list """ if not self._has_more_pages(): return [] if len(self._buffer): # if there is anything in the buffer returns that res = list(self._buffer) self._buffer.clear() return res else: # fetches the next block return self._fetch_next_block() def _fetch_next_block(self): raise NotImplementedError def __iter__(self): """Returns itself as an iterator""" return self def next(self): """Returns the next query result. :return: The next query result. :rtype: dict :raises StopIteration: If no more result is left. """ if self._has_finished: raise StopIteration if not len(self._buffer): results = self.fetch_next_block() self._buffer.extend(results) if not len(self._buffer): raise StopIteration return self._buffer.popleft() def __next__(self): # supports python 3 iterator return self.next() def _fetch_items_helper_no_retries(self, fetch_function): """Fetches more items and doesn't retry on failure :return: List of fetched items. :rtype: list """ fetched_items = [] # Continues pages till finds a non empty page or all results are exhausted while self._continuation or not self._has_started: if not self._has_started: self._has_started = True self._options['continuation'] = self._continuation (fetched_items, response_headers) = fetch_function(self._options) fetched_items continuation_key = http_constants.HttpHeaders.Continuation # Use Etag as continuation token for change feed queries. if self._is_change_feed: continuation_key = http_constants.HttpHeaders.ETag # In change feed queries, the continuation token is always populated. The hasNext() test is whether # there is any items in the response or not. if not self._is_change_feed or len(fetched_items) > 0: self._continuation = response_headers.get(continuation_key) else: self._continuation = None if fetched_items: break return fetched_items def _fetch_items_helper_with_retries(self, fetch_function): def callback(): return self._fetch_items_helper_no_retries(fetch_function) return retry_utility._Execute(self._client, self._client._global_endpoint_manager, callback) class _DefaultQueryExecutionContext(_QueryExecutionContextBase): """ This is the default execution context. """ def __init__(self, client, options, fetch_function): """ Constructor :param CosmosClient client: :param dict options: The request options for the request. :param method fetch_function: Will be invoked for retrieving each page Example of `fetch_function`: >>> def result_fn(result): >>> return result['Databases'] """ super(_DefaultQueryExecutionContext, self).__init__(client, options) self._fetch_function = fetch_function def _fetch_next_block(self): while super(_DefaultQueryExecutionContext, self)._has_more_pages() and len(self._buffer) == 0: return self._fetch_items_helper_with_retries(self._fetch_function) class _MultiCollectionQueryExecutionContext(_QueryExecutionContextBase): """ This class is used if it is client side partitioning """ def __init__(self, client, options, database_link, query, partition_key): """ Constructor :param CosmosClient client: :param dict options: The request options for the request. :param str database_link: database self link or ID based link :param (str or dict) query: Partition_key (str): partition key for the query """ super(_MultiCollectionQueryExecutionContext, self).__init__(client, options) self._current_collection_index = 0 self._collection_links = [] self._collection_links_length = 0 self._query = query self._client = client partition_resolver = client.GetPartitionResolver(database_link) if(partition_resolver is None): raise ValueError(client.PartitionResolverErrorMessage) else: self._collection_links = partition_resolver.ResolveForRead(partition_key) self._collection_links_length = len(self._collection_links) if self._collection_links is None: raise ValueError("_collection_links is None.") if self._collection_links_length <= 0: raise ValueError("_collection_links_length is not greater than 0.") # Creating the QueryFeed for the first collection path = base.GetPathFromLink(self._collection_links[self._current_collection_index], 'docs') collection_id = base.GetResourceIdOrFullNameFromLink(self._collection_links[self._current_collection_index]) self._current_collection_index += 1 def fetch_fn(options): return client.QueryFeed(path, collection_id, query, options) self._fetch_function = fetch_fn def _has_more_pages(self): return not self._has_started or self._continuation or (self._collection_links and self._current_collection_index < self._collection_links_length) def _fetch_next_block(self): """Fetches the next block of query results. This iterates fetches the next block of results from the current collection link. Once the current collection results were exhausted. It moves to the next collection link. :return: List of fetched items. :rtype: list """ # Fetch next block of results by executing the query against the current document collection fetched_items = self._fetch_items_helper_with_retries(self._fetch_function) # If there are multiple document collections to query for(in case of partitioning), keep looping through each one of them, # creating separate feed queries for each collection and fetching the items while not fetched_items: if self._collection_links and self._current_collection_index < self._collection_links_length: path = base.GetPathFromLink(self._collection_links[self._current_collection_index], 'docs') collection_id = base.GetResourceIdOrFullNameFromLink(self._collection_links[self._current_collection_index]) self._continuation = None self._has_started = False def fetch_fn(options): return self._client.QueryFeed(path, collection_id, self._query, options) self._fetch_function = fetch_fn fetched_items = self._fetch_items_helper_with_retries(self._fetch_function) self._current_collection_index += 1 else: break return fetched_items azure-cosmos-python-3.1.1/azure/cosmos/execution_context/document_producer.py000066400000000000000000000226461352206500100277610ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for document producer implementation in the Azure Cosmos database service. """ import six import numbers from collections import deque from azure.cosmos import base from azure.cosmos.execution_context.base_execution_context import _DefaultQueryExecutionContext from six.moves import xrange class _DocumentProducer(object): '''This class takes care of handling of the results for one single partition key range. When handling an orderby query, MultiExecutionContextAggregator instantiates one instance of this class per target partition key range and aggregates the result of each. ''' def __init__(self, partition_key_target_range, client, collection_link, query, document_producer_comp, options): ''' Constructor ''' self._options = options self._partition_key_target_range = partition_key_target_range self._doc_producer_comp = document_producer_comp self._client = client self._buffer = deque() self._is_finished = False self._has_started = False self._cur_item = None # initiate execution context path = base.GetPathFromLink(collection_link, 'docs') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) def fetch_fn(options): return self._client.QueryFeed(path, collection_id, query, options, partition_key_target_range['id']) self._ex_context = _DefaultQueryExecutionContext(client, self._options, fetch_fn) def get_target_range(self): """Returns the target partition key range. :return: Target partition key range. :rtype: dict """ return self._partition_key_target_range def __iter__(self): return self def next(self): """ :return: The next result item. :rtype: dict :raises StopIteration: If there is no more result. """ if self._cur_item is not None: res = self._cur_item self._cur_item = None return res return next(self._ex_context) def __next__(self): # supports python 3 iterator return self.next() def peek(self): """ TODO: use more_itertools.peekable instead :return: The current result item. :rtype: dict. :raises StopIteration: If there is no current item. """ if self._cur_item is None: self._cur_item = next(self._ex_context) return self._cur_item def __lt__(self, other): return self._doc_producer_comp.compare(self, other) < 0 def _compare_helper(a, b): if a is None and b is None: return 0 return (a > b) - (a < b) class _PartitionKeyRangeDocumentProduerComparator(object): """ Provides a Comparator for document producers using the min value of the corresponding target partition. """ def __init__(self): pass def compare(self, doc_producer1, doc_producer2): return _compare_helper(doc_producer1.get_target_range()['minInclusive'], doc_producer2.get_target_range()['minInclusive']) class _OrderByHelper: @staticmethod def getTypeOrd(orderby_item): """Returns the ordinal of the value of the item pair in the dictionary. :param dict orderby_item: :return: 0 if the item_pair doesn't have any 'item' key 1 if the value is undefined 2 if the value is a boolean 4 if the value is a number 5 if the value is a str or a unicode :rtype: int """ if 'item' not in orderby_item: return 0 val = orderby_item['item'] if val is None: return 1 if isinstance(val, bool): return 2 if isinstance(val, numbers.Number): return 4 if isinstance(val, six.string_types): return 5 raise TypeError('unknown type' + str(val)) @staticmethod def getTypeStr(orderby_item): """Returns the string representation of the type :param dict orderby_item: :return: String representation of the type :rtype: str """ if 'item' not in orderby_item: return 'NoValue' val = orderby_item['item'] if val is None: return 'Null' if isinstance(val, bool): return 'Boolean' if isinstance(val, numbers.Number): return 'Number' if isinstance(val, six.string_types): return 'String' raise TypeError('unknown type' + str(val)) @staticmethod def compare(orderby_item1, orderby_item2): """compares the two orderby item pairs. :param dict orderby_item1: :param dict orderby_item2: :return: Integer comparison result. The comparator acts such that - if the types are different we get: Undefined value < Null < booleans < Numbers < Strings - if both arguments are of the same type: it simply compares the values. :rtype: int """ type1_ord = _OrderByHelper.getTypeOrd(orderby_item1) type2_ord = _OrderByHelper.getTypeOrd(orderby_item2) type_ord_diff = type1_ord - type2_ord if type_ord_diff: return type_ord_diff # the same type, if type1_ord == 0: return 0 return _compare_helper(orderby_item1['item'], orderby_item2['item']) class _OrderByDocumentProducerComparator(_PartitionKeyRangeDocumentProduerComparator): """ Provides a Comparator for document producers which respects orderby sort order. """ def __init__(self, sort_order): """Instantiates this class :param list sort_order: List of sort orders (i.e., Ascending, Descending) :ivar list sort_order: List of sort orders (i.e., Ascending, Descending) """ self._sort_order = sort_order def _peek_order_by_items(self, peek_result): return peek_result['orderByItems'] def compare(self, doc_producer1, doc_producer2): """Compares the given two instances of DocumentProducers. Based on the orderby query items and whether the sort order is Ascending or Descending compares the peek result of the two DocumentProducers. If the peek results are equal based on the sort order, this comparator compares the target partition key range of the two DocumentProducers. :param _DocumentProducer doc_producers1: first instance :param _DocumentProducer doc_producers2: first instance :return: Integer value of compare result. positive integer if doc_producers1 > doc_producers2 negative integer if doc_producers1 < doc_producers2 :rtype: int """ res1 = self._peek_order_by_items(doc_producer1.peek()) res2 = self._peek_order_by_items(doc_producer2.peek()) self._validate_orderby_items(res1, res2) for i in xrange(len(res1)): res = _OrderByHelper.compare(res1[i], res2[i]) if (res != 0): if self._sort_order[i] == 'Ascending': return res elif self._sort_order[i] == 'Descending': return -res return _PartitionKeyRangeDocumentProduerComparator.compare(self, doc_producer1, doc_producer2) def _validate_orderby_items(self, res1, res2): if (len(res1) != len(res2)): # error raise ValueError('orderByItems cannot have different size') if (len(res1) != len(self._sort_order)): # error raise ValueError('orderByItems cannot have a different size than sort orders.') for i in xrange(len(res1)): type1 = _OrderByHelper.getTypeStr(res1[i]) type2 = _OrderByHelper.getTypeStr(res2[i]) if type1 != type2: raise ValueError("Expected {}, but got {}.".format(type1, type2)) azure-cosmos-python-3.1.1/azure/cosmos/execution_context/endpoint_component.py000066400000000000000000000106261352206500100301350ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for query execution endpoint component implementation in the Azure Cosmos database service. """ import numbers from azure.cosmos.execution_context.aggregators import _AverageAggregator, _CountAggregator, _MaxAggregator, \ _MinAggregator, _SumAggregator class _QueryExecutionEndpointComponent(object): def __init__(self, execution_context): self._execution_context = execution_context def __iter__(self): return self def next(self): return next(self._execution_context) def __next__(self): # supports python 3 iterator return self.next() class _QueryExecutionOrderByEndpointComponent(_QueryExecutionEndpointComponent): """Represents an endpoint in handling an order by query. For each processed orderby result it returns 'payload' item of the result """ def __init__(self, execution_context): super(_QueryExecutionOrderByEndpointComponent, self).__init__(execution_context) def next(self): return next(self._execution_context)['payload'] class _QueryExecutionTopEndpointComponent(_QueryExecutionEndpointComponent): """Represents an endpoint in handling top query. It only returns as many results as top arg specified. """ def __init__(self, execution_context, top_count): super(_QueryExecutionTopEndpointComponent, self).__init__(execution_context) self._top_count = top_count def next(self): if (self._top_count > 0): res = next(self._execution_context) self._top_count -= 1 return res raise StopIteration class _QueryExecutionAggregateEndpointComponent(_QueryExecutionEndpointComponent): """Represents an endpoint in handling aggregate query. It returns only aggreated values. """ def __init__(self, execution_context, aggregate_operators): super(_QueryExecutionAggregateEndpointComponent, self).__init__(execution_context) self._local_aggregators = [] self._results = None self._result_index = 0 for operator in aggregate_operators: if operator == 'Average': self._local_aggregators.append(_AverageAggregator()) elif operator == 'Count': self._local_aggregators.append(_CountAggregator()) elif operator == 'Max': self._local_aggregators.append(_MaxAggregator()) elif operator == 'Min': self._local_aggregators.append(_MinAggregator()) elif operator == 'Sum': self._local_aggregators.append(_SumAggregator()) def next(self): for res in self._execution_context: for item in res: for operator in self._local_aggregators: if isinstance(item, dict) and len(item.keys()) > 0: operator.aggregate(item['item']) elif isinstance(item, numbers.Number): operator.aggregate(item) if self._results is None: self._results = [] for operator in self._local_aggregators: self._results.append(operator.get_result()) if self._result_index < len(self._results): res = self._results[self._result_index] self._result_index += 1 return res else: raise StopIteration azure-cosmos-python-3.1.1/azure/cosmos/execution_context/execution_dispatcher.py000066400000000000000000000156401352206500100304450ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for proxy query execution context implementation in the Azure Cosmos database service. """ import json from six.moves import xrange from azure.cosmos.errors import HTTPFailure from azure.cosmos.execution_context.base_execution_context import _QueryExecutionContextBase from azure.cosmos.execution_context.base_execution_context import _DefaultQueryExecutionContext from azure.cosmos.execution_context.query_execution_info import _PartitionedQueryExecutionInfo from azure.cosmos.execution_context import endpoint_component from azure.cosmos.execution_context import multi_execution_aggregator from azure.cosmos.http_constants import StatusCodes, SubStatusCodes class _ProxyQueryExecutionContext(_QueryExecutionContextBase): ''' This class represents a proxy execution context wrapper: - By default uses _DefaultQueryExecutionContext - if backend responds a 400 error code with a Query Execution Info it switches to _MultiExecutionContextAggregator ''' def __init__(self, client, resource_link, query, options, fetch_function): ''' Constructor ''' super(_ProxyQueryExecutionContext, self).__init__(client, options) self._execution_context = _DefaultQueryExecutionContext(client, options, fetch_function) self._resource_link = resource_link self._query = query self._fetch_function = fetch_function def next(self): """Returns the next query result. :return: The next query result. :rtype: dict :raises StopIteration: If no more result is left. """ try: return next(self._execution_context) except HTTPFailure as e: if self._is_partitioned_execution_info(e): query_execution_info = self._get_partitioned_execution_info(e) self._execution_context = self._create_pipelined_execution_context(query_execution_info) else: raise e return next(self._execution_context) def fetch_next_block(self): """Returns a block of results. This method only exists for backward compatibility reasons. (Because QueryIterable has exposed fetch_next_block api). :return: List of results. :rtype: list """ try: return self._execution_context.fetch_next_block() except HTTPFailure as e: if self._is_partitioned_execution_info(e): query_execution_info = self._get_partitioned_execution_info(e) self._execution_context = self._create_pipelined_execution_context(query_execution_info) else: raise e return self._execution_context.fetch_next_block() def _is_partitioned_execution_info(self, e): return e.status_code == StatusCodes.BAD_REQUEST and e.sub_status == SubStatusCodes.CROSS_PARTITION_QUERY_NOT_SERVABLE def _get_partitioned_execution_info(self, e): error_msg = json.loads(e._http_error_message) return _PartitionedQueryExecutionInfo(json.loads(error_msg['additionalErrorInfo'])) def _create_pipelined_execution_context(self, query_execution_info): assert self._resource_link, "code bug, resource_link has is required." execution_context_aggregator = multi_execution_aggregator._MultiExecutionContextAggregator(self._client, self._resource_link, self._query, self._options, query_execution_info) return _PipelineExecutionContext(self._client, self._options, execution_context_aggregator, query_execution_info) class _PipelineExecutionContext(_QueryExecutionContextBase): DEFAULT_PAGE_SIZE = 1000 def __init__(self, client, options, execution_context, query_execution_info): ''' Constructor ''' super(_PipelineExecutionContext, self).__init__(client, options) if options.get('maxItemCount'): self._page_size = options['maxItemCount'] else: self._page_size = _PipelineExecutionContext.DEFAULT_PAGE_SIZE self._execution_context = execution_context self._endpoint = endpoint_component._QueryExecutionEndpointComponent(execution_context) order_by = query_execution_info.get_order_by() if (order_by): self._endpoint = endpoint_component._QueryExecutionOrderByEndpointComponent(self._endpoint) top = query_execution_info.get_top() if not (top is None): self._endpoint = endpoint_component._QueryExecutionTopEndpointComponent(self._endpoint, top) aggregates = query_execution_info.get_aggregates() if aggregates: self._endpoint = endpoint_component._QueryExecutionAggregateEndpointComponent(self._endpoint, aggregates) def next(self): """Returns the next query result. :return: The next query result. :rtype: dict :raises StopIteration: If no more result is left. """ return next(self._endpoint) def fetch_next_block(self): """Returns a block of results. This method only exists for backward compatibility reasons. (Because QueryIterable has exposed fetch_next_block api). This method internally invokes next() as many times required to collect the requested fetch size. :return: List of results. :rtype: list """ results = [] for _ in xrange(self._page_size): try: results.append(next(self)) except StopIteration: # no more results break return results azure-cosmos-python-3.1.1/azure/cosmos/execution_context/multi_execution_aggregator.py000066400000000000000000000141331352206500100316470ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for multi execution context aggregator implementation in the Azure Cosmos database service. """ import heapq from azure.cosmos.execution_context.base_execution_context import _QueryExecutionContextBase from azure.cosmos.execution_context import document_producer from azure.cosmos.routing import routing_range class _MultiExecutionContextAggregator(_QueryExecutionContextBase): """This class is capable of queries which requires rewriting based on backend's returned query execution info. This class maintains the execution context for each partition key range and aggregates the corresponding results from each execution context. When handling an orderby query, _MultiExecutionContextAggregator instantiates one instance of DocumentProducer per target partition key range and aggregates the result of each. TODO improvement: this class needs to be parallelized """ class PriorityQueue: """Provides a Priority Queue abstraction data structure""" def __init__(self): self._heap = [] def pop(self): return heapq.heappop(self._heap) def push(self, item): heapq.heappush(self._heap, item) def peek(self): return self._heap[0] def size(self): return len(self._heap) def __init__(self, client, resource_link, query, options, partitioned_query_ex_info): ''' Constructor ''' super(_MultiExecutionContextAggregator, self).__init__(client, options) # use the routing provider in the client self._routing_provider = client._routing_map_provider self._client = client self._resource_link = resource_link self._query = query self._partitioned_query_ex_info = partitioned_query_ex_info self._sort_orders = partitioned_query_ex_info.get_order_by() if self._sort_orders: self._document_producer_comparator = document_producer._OrderByDocumentProducerComparator(self._sort_orders) else: self._document_producer_comparator = document_producer._PartitionKeyRangeDocumentProduerComparator() # will be a list of (parition_min, partition_max) tuples targetPartitionRanges = self._get_target_parition_key_range() targetPartitionQueryExecutionContextList = [] for partitionTargetRange in targetPartitionRanges: # create and add the child execution context for the target range targetPartitionQueryExecutionContextList.append(self._createTargetPartitionQueryExecutionContext(partitionTargetRange)) self._orderByPQ = _MultiExecutionContextAggregator.PriorityQueue() for targetQueryExContext in targetPartitionQueryExecutionContextList: try: """TODO: we can also use more_itertools.peekable to be more python friendly""" targetQueryExContext.peek() # if there are matching results in the target ex range add it to the priority queue self._orderByPQ.push(targetQueryExContext) except StopIteration: continue def next(self): """returns the next result :return: The next result. :rtype: dict :raises StopIteration: If no more result is left. """ if self._orderByPQ.size() > 0: targetRangeExContext = self._orderByPQ.pop() res = next(targetRangeExContext) try: """TODO: we can also use more_itertools.peekable to be more python friendly""" targetRangeExContext.peek() self._orderByPQ.push(targetRangeExContext) except StopIteration: pass return res raise StopIteration def fetch_next_block(self): raise NotImplementedError("You should use pipeline's fetch_next_block.") def _createTargetPartitionQueryExecutionContext(self, partition_key_target_range): rewritten_query = self._partitioned_query_ex_info.get_rewritten_query() if rewritten_query: rewritten_query if isinstance(self._query, dict): # this is a parameterized query, collect all the parameters query = dict(self._query) query["query"] = rewritten_query else: query = rewritten_query else: query = self._query return document_producer._DocumentProducer(partition_key_target_range, self._client, self._resource_link, query, self._document_producer_comparator, self._options) def _get_target_parition_key_range(self): query_ranges = self._partitioned_query_ex_info.get_query_ranges() return self._routing_provider.get_overlapping_ranges(self._resource_link, [routing_range._Range.ParseFromDict(range_as_dict) for range_as_dict in query_ranges]) azure-cosmos-python-3.1.1/azure/cosmos/execution_context/query_execution_info.py000066400000000000000000000062741352206500100305020ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for partitioned query execution info implementation in the Azure Cosmos database service. """ import six class _PartitionedQueryExecutionInfo(object): ''' Represents a wrapper helper for partitioned query execution info dictionary returned by the backend. ''' QueryInfoPath = 'queryInfo' TopPath = [QueryInfoPath, 'top'] OrderByPath = [QueryInfoPath, 'orderBy'] AggregatesPath = [QueryInfoPath, 'aggregates'] QueryRangesPath = 'queryRanges' RewrittenQueryPath = [QueryInfoPath, 'rewrittenQuery'] def __init__(self, query_execution_info): ''' Constructor :param dict query_execution_info: ''' self._query_execution_info = query_execution_info def get_top(self): """Returns the top count (if any) or None """ return self._extract(_PartitionedQueryExecutionInfo.TopPath) def get_order_by(self): """Returns order by items (if any) or None """ return self._extract(_PartitionedQueryExecutionInfo.OrderByPath) def get_aggregates(self): """Returns aggregators (if any) or None """ return self._extract(_PartitionedQueryExecutionInfo.AggregatesPath) def get_query_ranges(self): """Returns query partition ranges (if any) or None """ return self._extract(_PartitionedQueryExecutionInfo.QueryRangesPath) def get_rewritten_query(self): """Returns rewritten query or None (if any) """ rewrittenQuery = self._extract(_PartitionedQueryExecutionInfo.RewrittenQueryPath) if rewrittenQuery is not None: # Hardcode formattable filter to true for now rewrittenQuery = rewrittenQuery.replace('{documentdb-formattableorderbyquery-filter}', 'true') return rewrittenQuery def _extract(self, path): item = self._query_execution_info if isinstance(path, six.string_types): return item.get(path) for p in path: item = item.get(p) if item is None: return None return item azure-cosmos-python-3.1.1/azure/cosmos/global_endpoint_manager.py000066400000000000000000000167301352206500100253200ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for global endpoint manager implementation in the Azure Cosmos database service. """ from six.moves.urllib.parse import urlparse import threading import azure.cosmos.constants as constants import azure.cosmos.errors as errors from azure.cosmos.location_cache import LocationCache class _GlobalEndpointManager(object): """ This internal class implements the logic for endpoint management for geo-replicated database accounts. """ def __init__(self, client): self.Client = client self.EnableEndpointDiscovery = client.connection_policy.EnableEndpointDiscovery self.PreferredLocations = client.connection_policy.PreferredLocations self.DefaultEndpoint = client.url_connection self.refresh_time_interval_in_ms = self.get_refresh_time_interval_in_ms_stub() self.location_cache = LocationCache(self.PreferredLocations, self.DefaultEndpoint, self.EnableEndpointDiscovery, client.connection_policy.UseMultipleWriteLocations, self.refresh_time_interval_in_ms) self.refresh_needed = False self.refresh_lock = threading.RLock() self.last_refresh_time = 0 def get_refresh_time_interval_in_ms_stub(self): return constants._Constants.DefaultUnavailableLocationExpirationTime def get_write_endpoint(self): return self.location_cache.get_write_endpoint() def get_read_endpoint(self): return self.location_cache.get_read_endpoint() def resolve_service_endpoint(self, request): return self.location_cache.resolve_service_endpoint(request) def mark_endpoint_unavailable_for_read(self, endpoint): self.location_cache.mark_endpoint_unavailable_for_read(endpoint) def mark_endpoint_unavailable_for_write(self, endpoint): self.location_cache.mark_endpoint_unavailable_for_write(endpoint) def get_ordered_write_endpoints(self): return self.location_cache.get_ordered_write_endpoints() def get_ordered_read_endpoints(self): return self.location_cache.get_ordered_read_endpoints() def can_use_multiple_write_locations(self, request): return self.location_cache.can_use_multiple_write_locations_for_request(request) def force_refresh(self, database_account): self.refresh_needed = True self.refresh_endpoint_list(database_account) def refresh_endpoint_list(self, database_account): with self.refresh_lock: # if refresh is not needed or refresh is already taking place, return if not self.refresh_needed: return try: self._refresh_endpoint_list_private(database_account) except Exception as e: raise e def _refresh_endpoint_list_private(self, database_account = None): if database_account : self.location_cache.perform_on_database_account_read(database_account) self.refresh_needed = False if self.location_cache.should_refresh_endpoints() and self.location_cache.current_time_millis() - self.last_refresh_time > self.refresh_time_interval_in_ms: if not database_account: database_account = self._GetDatabaseAccount() self.location_cache.perform_on_database_account_read(database_account) self.last_refresh_time = self.location_cache.current_time_millis() self.refresh_needed = False def _GetDatabaseAccount(self): """Gets the database account first by using the default endpoint, and if that doesn't returns use the endpoints for the preferred locations in the order they are specified to get the database account. """ try: database_account = self._GetDatabaseAccountStub(self.DefaultEndpoint) return database_account # If for any reason(non-globaldb related), we are not able to get the database account from the above call to GetDatabaseAccount, # we would try to get this information from any of the preferred locations that the user might have specified(by creating a locational endpoint) # and keeping eating the exception until we get the database account and return None at the end, if we are not able to get that info from any endpoints except errors.HTTPFailure: for location_name in self.PreferredLocations: locational_endpoint = _GlobalEndpointManager.GetLocationalEndpoint(self.DefaultEndpoint, location_name) try: database_account = self._GetDatabaseAccountStub(locational_endpoint) return database_account except errors.HTTPFailure: pass return None def _GetDatabaseAccountStub(self, endpoint): """Stub for getting database account from the client which can be used for mocking purposes as well. """ return self.Client.GetDatabaseAccount(endpoint) @staticmethod def GetLocationalEndpoint(default_endpoint, location_name): # For default_endpoint like 'https://contoso.documents.azure.com:443/' parse it to generate URL format # This default_endpoint should be global endpoint(and cannot be a locational endpoint) and we agreed to document that endpoint_url = urlparse(default_endpoint) # hostname attribute in endpoint_url will return 'contoso.documents.azure.com' if endpoint_url.hostname is not None: hostname_parts = str(endpoint_url.hostname).lower().split('.') if hostname_parts is not None: # global_database_account_name will return 'contoso' global_database_account_name = hostname_parts[0] # Prepare the locational_database_account_name as contoso-EastUS for location_name 'East US' locational_database_account_name = global_database_account_name + '-' + location_name.replace(' ', '') # Replace 'contoso' with 'contoso-EastUS' and return locational_endpoint as https://contoso-EastUS.documents.azure.com:443/ locational_endpoint = default_endpoint.lower().replace(global_database_account_name, locational_database_account_name, 1) return locational_endpoint return None azure-cosmos-python-3.1.1/azure/cosmos/hash_partition_resolver.py000066400000000000000000000076651352206500100254320ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Hash partition resolver implementation in the Azure Cosmos database service. """ import azure.cosmos.murmur_hash as murmur_hash import azure.cosmos.consistent_hash_ring as consistent_hash_ring class HashPartitionResolver(object): """HashPartitionResolver implements partitioning based on the value of a hash function, allowing you to evenly distribute requests and data across a number of partitions. """ def __init__(self, partition_key_extractor, collection_links, default_number_of_virtual_nodes_per_collection = 128, hash_generator = None): """ :param lambda partition_key_extractor: Returning the partition key from the document passed. :param list collection_links: The links of collections participating in partitioning. :param int default_number_of_virtual_nodes_per_collection: Number of virtual nodes per collection. :param HashGenerator hash_generator: The hash generator to be used for hashing algorithm. """ if partition_key_extractor is None: raise ValueError("partition_key_extractor is None.") if collection_links is None: raise ValueError("collection_links is None.") if default_number_of_virtual_nodes_per_collection <= 0: raise ValueError("The number of virtual nodes per collection must greater than 0.") self.partition_key_extractor = partition_key_extractor self.collection_links = collection_links if hash_generator is None: hash_generator = murmur_hash._MurmurHash() self.consistent_hash_ring = consistent_hash_ring._ConsistentHashRing(self.collection_links, default_number_of_virtual_nodes_per_collection, hash_generator) def ResolveForCreate(self, document): """Resolves the collection for creating the document based on the partition key. :param dict document: The document to be created. :return: Collection Self link or Name based link which should handle the Create operation. :rtype: str """ if document is None: raise ValueError("document is None.") partition_key = self.partition_key_extractor(document) return self.consistent_hash_ring.GetCollectionNode(partition_key) def ResolveForRead(self, partition_key): """Resolves the collection for reading/querying the documents based on the partition key. :param dict document: The document to be read/queried. :return: Collection Self link(s) or Name based link(s) which should handle the Read operation. :rtype: list """ if partition_key is None: return self.collection_links else: return [self.consistent_hash_ring.GetCollectionNode(partition_key)] azure-cosmos-python-3.1.1/azure/cosmos/http_constants.py000066400000000000000000000315711352206500100235410ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """HTTP Constants in the Azure Cosmos database service. """ class HttpMethods: """Constants of http methods. """ Get = 'GET' Post = 'POST' Put = 'PUT' Delete = 'DELETE' Head = 'HEAD' Options = 'OPTIONS' class HttpHeaders: """Constants of http headers. """ Authorization = 'authorization' ETag = 'etag' MethodOverride = 'X-HTTP-Method' Slug = 'Slug' ContentType = 'Content-Type' LastModified = 'Last-Modified' ContentEncoding = 'Content-Encoding' CharacterSet = 'CharacterSet' UserAgent = 'User-Agent' IfModified_since = 'If-Modified-Since' IfMatch = 'If-Match' IfNoneMatch = 'If-None-Match' ContentLength = 'Content-Length' AcceptEncoding = 'Accept-Encoding' KeepAlive = 'Keep-Alive' CacheControl = 'Cache-Control' TransferEncoding = 'Transfer-Encoding' ContentLanguage = 'Content-Language' ContentLocation = 'Content-Location' ContentMd5 = 'Content-Md5' ContentRange = 'Content-Range' Accept = 'Accept' AcceptCharset = 'Accept-Charset' AcceptLanguage = 'Accept-Language' IfRange = 'If-Range' IfUnmodifiedSince = 'If-Unmodified-Since' MaxForwards = 'Max-Forwards' ProxyAuthorization = 'Proxy-Authorization' AcceptRanges = 'Accept-Ranges' ProxyAuthenticate = 'Proxy-Authenticate' RetryAfter = 'Retry-After' SetCookie = 'Set-Cookie' WwwAuthenticate = 'Www-Authenticate' Origin = 'Origin' Host = 'Host' AccessControlAllowOrigin = 'Access-Control-Allow-Origin' AccessControlAllowHeaders = 'Access-Control-Allow-Headers' KeyValueEncodingFormat = 'application/x-www-form-urlencoded' WrapAssertionFormat = 'wrap_assertion_format' WrapAssertion = 'wrap_assertion' WrapScope = 'wrap_scope' SimpleToken = 'SWT' HttpDate = 'date' Prefer = 'Prefer' Location = 'Location' Referer = 'referer' # Query Query = 'x-ms-documentdb-query' IsQuery = 'x-ms-documentdb-isquery' # Our custom DocDB headers Continuation = 'x-ms-continuation' PageSize = 'x-ms-max-item-count' # Request sender generated. Simply echoed by backend. ActivityId = 'x-ms-activity-id' PreTriggerInclude = 'x-ms-documentdb-pre-trigger-include' PreTriggerExclude = 'x-ms-documentdb-pre-trigger-exclude' PostTriggerInclude = 'x-ms-documentdb-post-trigger-include' PostTriggerExclude = 'x-ms-documentdb-post-trigger-exclude' IndexingDirective = 'x-ms-indexing-directive' SessionToken = 'x-ms-session-token' ConsistencyLevel = 'x-ms-consistency-level' XDate = 'x-ms-date' CollectionPartitionInfo = 'x-ms-collection-partition-info' CollectionServiceInfo = 'x-ms-collection-service-info' RetryAfterInMilliseconds = 'x-ms-retry-after-ms' IsFeedUnfiltered = 'x-ms-is-feed-unfiltered' ResourceTokenExpiry = 'x-ms-documentdb-expiry-seconds' EnableScanInQuery = 'x-ms-documentdb-query-enable-scan' EmitVerboseTracesInQuery = 'x-ms-documentdb-query-emit-traces' SubStatus = 'x-ms-substatus' AlternateContentPath = 'x-ms-alt-content-path' IsContinuationExpected = "x-ms-documentdb-query-iscontinuationexpected" PopulateQueryMetrics = "x-ms-documentdb-populatequerymetrics" # Quota Info MaxEntityCount = 'x-ms-root-entity-max-count' CurrentEntityCount = 'x-ms-root-entity-current-count' CollectionQuotaInMb = 'x-ms-collection-quota-mb' CollectionCurrentUsageInMb = 'x-ms-collection-usage-mb' MaxMediaStorageUsageInMB = 'x-ms-max-media-storage-usage-mb' # Collection quota PopulateQuotaInfo = 'x-ms-documentdb-populatequotainfo' PopulatePartitionKeyRangeStatistics = 'x-ms-documentdb-populatepartitionstatistics' # Usage Info CurrentMediaStorageUsageInMB = 'x-ms-media-storage-usage-mb' RequestCharge = 'x-ms-request-charge' #Address related headers. ForceRefresh = 'x-ms-force-refresh' ItemCount = 'x-ms-item-count' NewResourceId = 'x-ms-new-resource-id' UseMasterCollectionResolver = 'x-ms-use-master-collection-resolver' # Admin Headers FullUpgrade = 'x-ms-force-full-upgrade' OnlyUpgradeSystemApplications = 'x-ms-only-upgrade-system-applications' OnlyUpgradeNonSystemApplications = 'x-ms-only-upgrade-non-system-applications' UpgradeFabricRingCodeAndConfig = 'x-ms-upgrade-fabric-code-config' IgnoreInProgressUpgrade = 'x-ms-ignore-inprogress-upgrade' UpgradeVerificationKind = 'x-ms-upgrade-verification-kind' IsCanary = 'x-ms-iscanary' # Version headers and values Version = 'x-ms-version' # RDFE Resource Provider headers OcpResourceProviderRegisteredUri = 'ocp-resourceprovider-registered-uri' # For Document service management operations only. This is in # essence a 'handle' to (long running) operations. RequestId = 'x-ms-request-id' # Object returning this determines what constitutes state and what # last state change means. For replica, it is the last role change. LastStateChangeUtc = 'x-ms-last-state-change-utc' # Offer type. OfferType = 'x-ms-offer-type' OfferThroughput = "x-ms-offer-throughput" # Custom RUs/minute headers DisableRUPerMinuteUsage = "x-ms-documentdb-disable-ru-per-minute-usage" IsRUPerMinuteUsed = "x-ms-documentdb-is-ru-per-minute-used" OfferIsRUPerMinuteThroughputEnabled = "x-ms-offer-is-ru-per-minute-throughput-enabled" # Partitioned collection headers PartitionKey = "x-ms-documentdb-partitionkey" EnableCrossPartitionQuery = "x-ms-documentdb-query-enablecrosspartition" PartitionKeyRangeID = 'x-ms-documentdb-partitionkeyrangeid' # Upsert header IsUpsert = 'x-ms-documentdb-is-upsert' # Index progress headers. IndexTransformationProgress = 'x-ms-documentdb-collection-index-transformation-progress' LazyIndexingProgress = 'x-ms-documentdb-collection-lazy-indexing-progress' # Client generated retry count response header ThrottleRetryCount = 'x-ms-throttle-retry-count' ThrottleRetryWaitTimeInMs = 'x-ms-throttle-retry-wait-time-ms' # StoredProcedure related headers EnableScriptLogging = 'x-ms-documentdb-script-enable-logging' ScriptLogResults = 'x-ms-documentdb-script-log-results' # Change feed AIM = 'A-IM' IncrementalFeedHeaderValue = 'Incremental feed' # For Using Multiple Write Locations AllowTentativeWrites = "x-ms-cosmos-allow-tentative-writes" class HttpHeaderPreferenceTokens: """Constants of http header preference tokens. """ PreferUnfilteredQueryResponse = 'PreferUnfilteredQueryResponse' class HttpStatusDescriptions: """Constants of http status descriptions. """ Accepted = 'Accepted' Conflict = 'Conflict' OK = 'Ok' PreconditionFailed = 'Precondition Failed' NotModified = 'Not Modified' NotFound = 'Not Found' BadGateway = 'Bad Gateway' BadRequest = 'Bad Request' InternalServerError = 'Internal Server Error' MethodNotAllowed = 'MethodNotAllowed' NotAcceptable = 'Not Acceptable' NoContent = 'No Content' Created = 'Created' UnsupportedMediaType = 'Unsupported Media Type' LengthRequired = 'Length Required' ServiceUnavailable = 'Service Unavailable' RequestEntityTooLarge = 'Request Entity Too Large' Unauthorized = 'Unauthorized' Forbidden = 'Forbidden' Gone = 'Gone' RequestTimeout = 'Request timed out' GatewayTimeout = 'Gateway timed out' TooManyRequests = 'Too Many Requests' RetryWith = 'Retry the request' class QueryStrings: """Constants of query strings. """ Filter = '$filter' GenerateId = '$generateFor' GenerateIdBatchSize = '$batchSize' GetChildResourcePartitions = '$getChildResourcePartitions' Url = '$resolveFor' RootIndex = '$rootIndex' Query = 'query' SQLQueryType = 'sql' # RDFE Resource Provider query strings ContentView = 'contentview' Generic = 'generic' class CookieHeaders: """Constants of cookie headers. """ SessionToken = 'x-ms-session-token' class Versions: """Constants of versions. """ CurrentVersion = '2018-09-17' SDKName = 'azure-cosmos' SDKVersion = '3.1.1' class Delimiters: """Constants of delimiters. """ ClientContinuationDelimiter = '!!' ClientContinuationFormat = '{0}!!{1}' class HttpListenerErrorCodes: """Constants of http listener error codes. """ ERROR_OPERATION_ABORTED = 995 ERROR_CONNECTION_INVALID = 1229 class HttpContextProperties: """Constants of http context properties. """ SubscriptionId = 'SubscriptionId' class _ErrorCodes: """Windows Socket Error Codes """ WindowsInterruptedFunctionCall = 10004 WindowsFileHandleNotValid = 10009 WindowsPermissionDenied = 10013 WindowsBadAddress = 10014 WindowsInvalidArgumnet = 10022 WindowsResourceTemporarilyUnavailable = 10035 WindowsOperationNowInProgress = 10036 WindowsAddressAlreadyInUse = 10048 WindowsConnectionResetByPeer = 10054 WindowsCannotSendAfterSocketShutdown = 10058 WindowsConnectionTimedOut = 10060 WindowsConnectionRefused = 10061 WindowsNameTooLong = 10063 WindowsHostIsDown = 10064 WindowsNoRouteTohost = 10065 """Linux Error Codes """ LinuxConnectionReset = 131 class StatusCodes: """HTTP status codes returned by the REST operations """ # Success OK = 200 CREATED = 201 ACCEPTED = 202 NO_CONTENT = 204 NOT_MODIFIED = 304 # Client Error BAD_REQUEST = 400 UNAUTHORIZED = 401 FORBIDDEN = 403 NOT_FOUND = 404 METHOD_NOT_ALLOWED = 405 REQUEST_TIMEOUT = 408 CONFLICT = 409 GONE = 410 PRECONDITION_FAILED = 412 REQUEST_ENTITY_TOO_LARGE = 413 TOO_MANY_REQUESTS = 429 RETRY_WITH = 449 INTERNAL_SERVER_ERROR = 500 SERVICE_UNAVAILABLE = 503 # Operation pause and cancel. These are FAKE status codes for QOS logging purpose only. OPERATION_PAUSED = 1200 OPERATION_CANCELLED = 1201 class SubStatusCodes: """Sub status codes returned by the REST operations specifying the details of the operation """ UNKNOWN = 0 # 400: Bad Request Substatus PARTITION_KEY_MISMATCH = 1001 CROSS_PARTITION_QUERY_NOT_SERVABLE = 1004 # 410: StatusCodeType_Gone: substatus NAME_CACHE_IS_STALE = 1000 PARTITION_KEY_RANGE_GONE = 1002 COMPLETING_SPLIT = 1007 COMPLETING_PARTITION_MIGRATION = 1008 # 403: Forbidden Substatus. WRITE_FORBIDDEN = 3 PROVISION_LIMIT_REACHED = 1005 DATABASE_ACCOUNT_NOT_FOUND = 1008 REDUNDANT_COLLECTION_PUT = 1009 SHARED_THROUGHPUT_DATABASE_QUOTA_EXCEEDED = 1010 SHARED_THROUGHPUT_OFFER_GROW_NOT_NEEDED = 1011 # 404: LSN in session token is higher READ_SESSION_NOTAVAILABLE = 1002 OWNER_RESOURCE_NOT_FOUND = 1003 # 409: Conflict exception CONFLICT_WITH_CONTROL_PLANE = 1006 # 503: Service Unavailable due to region being out of capacity for bindable partitions INSUFFICIENT_BINDABLE_PARTITIONS = 1007 class ResourceType: """Types of resources in Azure Cosmos """ Database = "dbs" Collection = "colls" User = "users" Document = "docs" Permission = "permissions" StoredProcedure = "sprocs" Trigger = "triggers" UserDefinedFunction = "udfs" Conflict = "conflicts" Attachment = "attachments" PartitionKeyRange = "pkranges" Schema = "schemas" Offer = "offers" Topology = "topology" DatabaseAccount = "databaseaccount" Media = "media" @staticmethod def IsCollectionChild(resourceType): return (resourceType == ResourceType.Document or resourceType == ResourceType.Attachment or resourceType == ResourceType.Conflict or resourceType == ResourceType.Schema or resourceType == ResourceType.UserDefinedFunction or resourceType == ResourceType.Trigger or resourceType == ResourceType.StoredProcedure) azure-cosmos-python-3.1.1/azure/cosmos/location_cache.py000066400000000000000000000335351352206500100234230ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2018 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Implements the abstraction to resolve target location for geo-replicated DatabaseAccount with multiple writable and readable locations. """ import collections import time import azure.cosmos.base as base import azure.cosmos.documents as documents import azure.cosmos.http_constants as http_constants class EndpointOperationType(object): NoneType = "None" ReadType = "Read" WriteType = "Write" class LocationCache(object): def current_time_millis(self): return int(round(time.time() * 1000)) def __init__(self, preferred_locations, default_endpoint, enable_endpoint_discovery, use_multiple_write_locations, refresh_time_interval_in_ms): self.preferred_locations = preferred_locations self.default_endpoint = default_endpoint self.enable_endpoint_discovery = enable_endpoint_discovery self.use_multiple_write_locations = use_multiple_write_locations self.enable_multiple_writable_locations = False self.write_endpoints = [self.default_endpoint] self.read_endpoints = [self.default_endpoint] self.location_unavailability_info_by_endpoint = {} self.refresh_time_interval_in_ms = refresh_time_interval_in_ms self.last_cache_update_time_stamp = 0 self.available_read_endpoint_by_locations = {} self.available_write_endpoint_by_locations = {} self.available_write_locations = [] self.available_read_locations = [] def check_and_update_cache(self): if (len(self.location_unavailability_info_by_endpoint) > 0 and self.current_time_millis() - self.last_cache_update_time_stamp > self.refresh_time_interval_in_ms): self.update_location_cache() def get_write_endpoints(self): self.check_and_update_cache() return self.write_endpoints def get_read_endpoints(self): self.check_and_update_cache() return self.read_endpoints def get_write_endpoint(self): return self.get_write_endpoints()[0] def get_read_endpoint(self): return self.get_read_endpoints()[0] def mark_endpoint_unavailable_for_read(self, endpoint): self.mark_endpoint_unavailable(endpoint, EndpointOperationType.ReadType) def mark_endpoint_unavailable_for_write(self, endpoint): self.mark_endpoint_unavailable(endpoint, EndpointOperationType.WriteType) def perform_on_database_account_read(self, database_account): self.update_location_cache(database_account._WritableLocations, database_account._ReadableLocations, database_account._EnableMultipleWritableLocations) def get_ordered_write_endpoints(self): return self.available_write_locations def get_ordered_read_endpoints(self): return self.available_read_locations def resolve_service_endpoint(self, request): if request.location_endpoint_to_route: return request.location_endpoint_to_route location_index = int(request.location_index_to_route) if request.location_index_to_route else 0 use_preferred_locations = request.use_preferred_locations if request.use_preferred_locations is not None else True if (not use_preferred_locations or (documents._OperationType.IsWriteOperation(request.operation_type) and not self.can_use_multiple_write_locations_for_request(request))): # For non-document resource types in case of client can use multiple write locations # or when client cannot use multiple write locations, flip-flop between the # first and the second writable region in DatabaseAccount (for manual failover) if self.enable_endpoint_discovery and len(self.available_write_locations) > 0: location_index = min(location_index % 2, len(self.available_write_locations) - 1) write_location = self.available_write_locations[location_index] return self.available_write_endpoint_by_locations[write_location] else: return self.default_endpoint else: endpoints = self.get_write_endpoints() if documents._OperationType.IsWriteOperation(request.operation_type) else self.get_read_endpoints() return endpoints[location_index % len(endpoints)] def should_refresh_endpoints(self): most_preferred_location = self.preferred_locations[0] if (self.preferred_locations and len(self.preferred_locations) > 0) else None # we should schedule refresh in background if we are unable to target the user's most preferredLocation. if self.enable_endpoint_discovery: should_refresh = self.use_multiple_write_locations and not self.enable_multiple_writable_locations if most_preferred_location: if self.available_read_endpoint_by_locations: most_preferred_read_endpoint = self.available_read_endpoint_by_locations[most_preferred_location] if most_preferred_read_endpoint and most_preferred_read_endpoint != self.read_endpoints[0]: # For reads, we can always refresh in background as we can alternate to # other available read endpoints return True else: return True if not self.can_use_multiple_write_locations(): if self.is_endpoint_unavailable(self.write_endpoints[0], EndpointOperationType.WriteType): # Since most preferred write endpoint is unavailable, we can only refresh in background if # we have an alternate write endpoint return True else: return should_refresh elif most_preferred_location: most_preferred_write_endpoint = self.available_write_endpoint_by_locations[most_preferred_location] if most_preferred_write_endpoint: should_refresh |= most_preferred_write_endpoint != self.write_endpoints[0] return should_refresh else: return True else: return should_refresh else: return False def clear_stale_endpoint_unavailability_info(self): new_location_unavailability_info = {} if len(self.location_unavailability_info_by_endpoint) > 0: for unavailable_endpoint in self.location_unavailability_info_by_endpoint: unavailability_info = self.location_unavailability_info_by_endpoint[unavailable_endpoint] if not (unavailability_info and self.current_time_millis() - unavailability_info['lastUnavailabilityCheckTimeStamp'] > self.refresh_time_interval_in_ms): new_location_unavailability_info[unavailable_endpoint] = self.location_unavailability_info_by_endpoint[unavailable_endpoint] self.location_unavailability_info_by_endpoint = new_location_unavailability_info def is_endpoint_unavailable(self, endpoint, expected_available_operations): unavailability_info = self.location_unavailability_info_by_endpoint[endpoint] if endpoint in self.location_unavailability_info_by_endpoint else None if (expected_available_operations == EndpointOperationType.NoneType or not unavailability_info or expected_available_operations not in unavailability_info['operationType']): return False else: if (self.current_time_millis() - unavailability_info['lastUnavailabilityCheckTimeStamp'] > self.refresh_time_interval_in_ms): return False else: # Unexpired entry present. Endpoint is unavailable return True def mark_endpoint_unavailable(self, unavailable_endpoint, unavailable_operation_type): unavailablility_info = self.location_unavailability_info_by_endpoint[unavailable_endpoint] if unavailable_endpoint in self.location_unavailability_info_by_endpoint else None current_time = self.current_time_millis() if not unavailablility_info: self.location_unavailability_info_by_endpoint[unavailable_endpoint] = {'lastUnavailabilityCheckTimeStamp': current_time, 'operationType': set([unavailable_operation_type])} else: unavailable_operations = set([unavailable_operation_type]).union(unavailablility_info['operationType']) self.location_unavailability_info_by_endpoint[unavailable_endpoint] = {'lastUnavailabilityCheckTimeStamp': current_time,'operationType': unavailable_operations} self.update_location_cache() def get_preferred_locations(self): return self.preferred_locations def update_location_cache(self, write_locations = None, read_locations = None, enable_multiple_writable_locations = None): if enable_multiple_writable_locations: self.enable_multiple_writable_locations = enable_multiple_writable_locations self.clear_stale_endpoint_unavailability_info() if self.enable_endpoint_discovery: if read_locations: self.available_read_endpoint_by_locations, self.available_read_locations = self.get_endpoint_by_location(read_locations) if write_locations: self.available_write_endpoint_by_locations, self.available_write_locations = self.get_endpoint_by_location(write_locations) self.write_endpoints = self.get_preferred_available_endpoints(self.available_write_endpoint_by_locations, self.available_write_locations, EndpointOperationType.WriteType, self.default_endpoint) self.read_endpoints = self.get_preferred_available_endpoints(self.available_read_endpoint_by_locations, self.available_read_locations, EndpointOperationType.ReadType, self.write_endpoints[0]) self.last_cache_update_timestamp = self.current_time_millis() def get_preferred_available_endpoints(self, endpoints_by_location, orderedLocations, expected_available_operation, fallback_endpoint): endpoints = [] # if enableEndpointDiscovery is false, we always use the defaultEndpoint that user passed in during documentClient init if self.enable_endpoint_discovery and endpoints_by_location: if self.can_use_multiple_write_locations() or expected_available_operation == EndpointOperationType.ReadType: unavailable_endpoints = [] if self.preferred_locations: # When client can not use multiple write locations, preferred locations list should only be used # determining read endpoints order. # If client can use multiple write locations, preferred locations list should be used for determining # both read and write endpoints order. for location in self.preferred_locations: endpoint = endpoints_by_location[location] if location in endpoints_by_location else None if endpoint: if self.is_endpoint_unavailable(endpoint, expected_available_operation): unavailable_endpoints.append(endpoint) else: endpoints.append(endpoint) if len(endpoints)== 0: endpoints.append(fallback_endpoint) endpoints.extend(unavailable_endpoints) else: for location in orderedLocations: if location and location in endpoints_by_location: # location is empty during manual failover endpoints.append(endpoints_by_location[location]) if len(endpoints) == 0: endpoints.append(fallback_endpoint) return endpoints def get_endpoint_by_location(self, locations): endpoints_by_location = collections.OrderedDict() parsed_locations = [] for location in locations: if not location['name']: # during fail-over the location name is empty continue try: region_uri = location['databaseAccountEndpoint'] parsed_locations.append(location['name']) endpoints_by_location.update({location['name']: region_uri}) except Exception as e: raise e return endpoints_by_location, parsed_locations def can_use_multiple_write_locations(self): return self.use_multiple_write_locations and self.enable_multiple_writable_locations def can_use_multiple_write_locations_for_request(self, request): return self.can_use_multiple_write_locations() and (request.resource_type == http_constants.ResourceType.Document or (request.resource_type == http_constants.ResourceType.StoredProcedure and request.operation_type == documents._OperationType.ExecuteJavaScript)) azure-cosmos-python-3.1.1/azure/cosmos/murmur_hash.py000066400000000000000000000100611352206500100230070ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for Murmur hash implementation in the Azure Cosmos database service. """ from struct import pack, unpack from six.moves import xrange ''' pymmh3 was written by Fredrik Kihlander, and is placed in the public domain. The author hereby disclaims copyright to this source code. pure python implementation of the murmur3 hash algorithm https://code.google.com/p/smhasher/wiki/MurmurHash3 This was written for the times when you do not want to compile c-code and install modules, and you only want a drop-in murmur3 implementation. As this is purely python it is FAR from performant and if performance is anything that is needed a proper c-module is suggested! This module is written to have the same format as mmh3 python package found here for simple conversions: https://pypi.python.org/pypi/mmh3/2.0 ''' class _MurmurHash(object): """ The 32 bit x86 version of MurmurHash3 implementation. """ def ComputeHash(self, key): """ Computes the hash of the value passed using MurmurHash3 algorithm. :param bytearray key: Byte array representing the key to be hashed. :return: 32 bit hash value. :rtype: int """ if key is None: raise ValueError("key is None.") hash_value = self._ComputeHash(key) return bytearray(pack('I', hash_value)) @staticmethod def _ComputeHash( key, seed = 0x0 ): """Computes the hash of the value passed using MurmurHash3 algorithm with the seed value. """ def fmix( h ): h ^= h >> 16 h = ( h * 0x85ebca6b ) & 0xFFFFFFFF h ^= h >> 13 h = ( h * 0xc2b2ae35 ) & 0xFFFFFFFF h ^= h >> 16 return h length = len( key ) nblocks = int( length / 4 ) h1 = seed c1 = 0xcc9e2d51 c2 = 0x1b873593 # body for block_start in xrange( 0, nblocks * 4, 4 ): k1 = key[ block_start + 3 ] << 24 | \ key[ block_start + 2 ] << 16 | \ key[ block_start + 1 ] << 8 | \ key[ block_start + 0 ] k1 = c1 * k1 & 0xFFFFFFFF k1 = ( k1 << 15 | k1 >> 17 ) & 0xFFFFFFFF # inlined ROTL32 k1 = ( c2 * k1 ) & 0xFFFFFFFF h1 ^= k1 h1 = ( h1 << 13 | h1 >> 19 ) & 0xFFFFFFFF # inlined _ROTL32 h1 = ( h1 * 5 + 0xe6546b64 ) & 0xFFFFFFFF # tail tail_index = nblocks * 4 k1 = 0 tail_size = length & 3 if tail_size >= 3: k1 ^= key[ tail_index + 2 ] << 16 if tail_size >= 2: k1 ^= key[ tail_index + 1 ] << 8 if tail_size >= 1: k1 ^= key[ tail_index + 0 ] if tail_size != 0: k1 = ( k1 * c1 ) & 0xFFFFFFFF k1 = ( k1 << 15 | k1 >> 17 ) & 0xFFFFFFFF # _ROTL32 k1 = ( k1 * c2 ) & 0xFFFFFFFF h1 ^= k1 return fmix( h1 ^ length ) azure-cosmos-python-3.1.1/azure/cosmos/partition.py000066400000000000000000000051141352206500100224710ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for client side partition implementation in the Azure Cosmos database service. """ from six.moves import xrange class _Partition(object): """Represents a class that holds the hash value and node name for each partition. """ def __init__(self, hash_value = None, node = None): self.hash_value = hash_value self.node = node def GetNode(self): """Gets the name of the node(collection) for this object. """ return self.node def __eq__(self, other): return (self.hash_value == other.hash_value) and (self.node == other.node) def __lt__(self, other): if self == other: return False return self.CompareTo(other.hash_value) < 0 def CompareTo(self, other_hash_value): """Compares the passed hash value with the hash value of this object """ if len(self.hash_value) != len(other_hash_value): raise ValueError("Length of hashes doesn't match.") # The hash byte array that is returned from ComputeHash method has the MSB at the end of the array # so comparing the bytes from the end for compare operations. for i in xrange(0, len(self.hash_value)): if(self.hash_value[len(self.hash_value) - i - 1] < other_hash_value[len(self.hash_value) - i - 1]): return -1 elif self.hash_value[len(self.hash_value) - i - 1] > other_hash_value[len(self.hash_value) - i - 1]: return 1 return 0 azure-cosmos-python-3.1.1/azure/cosmos/query_iterable.py000066400000000000000000000121371352206500100234770ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Iterable query results in the Azure Cosmos database service. """ from azure.cosmos.execution_context import execution_dispatcher from azure.cosmos.execution_context import base_execution_context class QueryIterable(object): """Represents an iterable object of the query results. QueryIterable is a wrapper for query execution context. """ def __init__(self, client, query, options, fetch_function, collection_link = None): """ Instantiates a QueryIterable for non-client side partitioning queries. _ProxyQueryExecutionContext will be used as the internal query execution context :param CosmosClient client: Instance of document client. :param (str or dict) query: :param dict options: The request options for the request. :param method fetch_function: :param str collection_link: If this is a Document query/feed collection_link is required. Example of `fetch_function`: >>> def result_fn(result): >>> return result['Databases'] """ self._client = client self.retry_options = client.connection_policy.RetryOptions self._query = query self._options = options self._fetch_function = fetch_function self._collection_link = collection_link self._ex_context = None @classmethod def PartitioningQueryIterable(cls, client, query, options, database_link, partition_key): """ Represents a client side partitioning query iterable. This constructor instantiates a QueryIterable for client side partitioning queries, and sets _MultiCollectionQueryExecutionContext as the internal execution context. :param CosmosClient client: Instance of document client :param (str or dict) options: :param dict options: The request options for the request. :param str database_link: Database self link or ID based link :param str partition_key: Partition key for the query """ # This will call the base constructor(__init__ method above) self = cls(client, query, options, None, None) self._database_link = database_link self._partition_key = partition_key return self def _create_execution_context(self): """instantiates the internal query execution context based. """ if hasattr(self, '_database_link'): # client side partitioning query return base_execution_context._MultiCollectionQueryExecutionContext(self._client, self._options, self._database_link, self._query, self._partition_key) else: # return execution_dispatcher._ProxyQueryExecutionContext(self._client, self._collection_link, self._query, self._options, self._fetch_function) def __iter__(self): """Makes this class iterable. """ return self.Iterator(self) class Iterator(object): def __init__(self, iterable): self._iterable = iterable self._finished = False self._ex_context = iterable._create_execution_context() def __iter__(self): # Always returns self return self def __next__(self): return next(self._ex_context) # Also support Python 3.x iteration def next(self): return self.__next__() def fetch_next_block(self): """Returns a block of results with respecting retry policy. This method only exists for backward compatibility reasons. (Because QueryIterable has exposed fetch_next_block api). :return: List of results. :rtype: list """ if self._ex_context is None: # initiates execution context for the first time self._ex_context = self._create_execution_context() return self._ex_context.fetch_next_block() azure-cosmos-python-3.1.1/azure/cosmos/range.py000066400000000000000000000055471352206500100215660ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Range class implementation in the Azure Cosmos database service. """ class Range(object): """Represents the Range class used to map the partition key of the document to their associated collection. """ def __init__(self, low, high): if low is None: raise ValueError("low is None.") if high is None: raise ValueError("high is None.") if(low > high): raise ValueError("Range low value must be less than or equal the high value.") self.low = low self.high = high def __hash__(self): return hash((self.low, self.high)) def __str__(self): return str(self.low) + str(self.high) def __eq__(self, other): return (self.low == other.low) and (self.high == other.high) def __lt__(self, other): if self == other: return False elif self.low < other.low or self.high < other.high: return True else: return False def Contains(self, other): """Checks if the passed parameter is in the range of this object. """ if other is None: raise ValueError("other is None.") if isinstance(other, Range): if other.low >= self.low and other.high <= self.high: return True return False else: return self.Contains(Range(other, other)) def Intersect(self, other): """Checks if the passed parameter intersects the range of this object. """ if isinstance(other, Range): max_low = self.low if (self.low >= other.low) else other.low min_high = self.high if (self.high <= other.high) else other.high if max_low <= min_high: return True return False azure-cosmos-python-3.1.1/azure/cosmos/range_partition_resolver.py000066400000000000000000000114151352206500100255670ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Range partition resolver implementation in the Azure Cosmos database service. """ import azure.cosmos.range as prange class RangePartitionResolver(object): """RangePartitionResolver implements partitioning based on the ranges, allowing you to distribute requests and data across a number of partitions. """ def __init__(self, partition_key_extractor, partition_map): """ :param lambda partition_key_extractor: Returning the partition key from the document passed. :param dict partition_map: The dictionary of ranges mapped to their associated collection """ if partition_key_extractor is None: raise ValueError("partition_key_extractor is None.") if partition_map is None: raise ValueError("partition_map is None.") self.partition_key_extractor = partition_key_extractor self.partition_map = partition_map def ResolveForCreate(self, document): """Resolves the collection for creating the document based on the partition key. :param dict document: The document to be created. :return: Collection Self link or Name based link which should handle the Create operation. :rtype: str """ if document is None: raise ValueError("document is None.") partition_key = self.partition_key_extractor(document) containing_range = self._GetContainingRange(partition_key) if containing_range is None: raise ValueError("A containing range for " + str(partition_key) + " doesn't exist in the partition map.") return self.partition_map.get(containing_range) def ResolveForRead(self, partition_key): """Resolves the collection for reading/querying the documents based on the partition key. :param dict document: The document to be read/queried. :return: Collection Self link(s) or Name based link(s) which should handle the Read operation. :rtype: list """ intersecting_ranges = self._GetIntersectingRanges(partition_key) collection_links = list() for keyrange in intersecting_ranges: collection_links.append(self.partition_map.get(keyrange)) return collection_links def _GetContainingRange(self, partition_key): """Gets the containing range based on the partition key. """ for keyrange in self.partition_map.keys(): if keyrange.Contains(partition_key): return keyrange return None def _GetIntersectingRanges(self, partition_key): """Gets the intersecting ranges based on the partition key. """ partitionkey_ranges = set() intersecting_ranges = set() if partition_key is None: return list(self.partition_map.keys()) if isinstance(partition_key, prange.Range): partitionkey_ranges.add(partition_key) elif isinstance(partition_key, list): for key in partition_key: if key is None: return list(self.partition_map.keys()) elif isinstance(key, prange.Range): partitionkey_ranges.add(key) else: partitionkey_ranges.add(prange.Range(key, key)) else: partitionkey_ranges.add(prange.Range(partition_key, partition_key)) for partitionKeyRange in partitionkey_ranges: for keyrange in self.partition_map.keys(): if keyrange.Intersect(partitionKeyRange): intersecting_ranges.add(keyrange) return intersecting_rangesazure-cosmos-python-3.1.1/azure/cosmos/request_object.py000066400000000000000000000043021352206500100234740ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2018 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Represents a request object. """ class _RequestObject(object): def __init__(self, resource_type, operation_type, endpoint_override = None): self.resource_type = resource_type self.operation_type = operation_type self.endpoint_override = endpoint_override self.should_clear_session_token_on_session_read_failure = False self.use_preferred_locations = None self.location_index_to_route = None self.location_endpoint_to_route = None def route_to_location_with_preferred_location_flag(self, location_index, use_preferred_locations): self.location_index_to_route = location_index self.use_preferred_locations = use_preferred_locations self.location_endpoint_to_route = None def route_to_location(self, location_endpoint): self.location_index_to_route = None self.use_preferred_locations = None self.location_endpoint_to_route = location_endpoint def clear_route_to_location(self): self.location_index_to_route = None self.use_preferred_locations = None self.location_endpoint_to_route = None azure-cosmos-python-3.1.1/azure/cosmos/resource_throttle_retry_policy.py000066400000000000000000000054041352206500100270420ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for resource throttle retry policy implementation in the Azure Cosmos database service. """ import azure.cosmos.http_constants as http_constants class _ResourceThrottleRetryPolicy(object): def __init__(self, max_retry_attempt_count, fixed_retry_interval_in_milliseconds, max_wait_time_in_seconds): self._max_retry_attempt_count = max_retry_attempt_count self._fixed_retry_interval_in_milliseconds = fixed_retry_interval_in_milliseconds self._max_wait_time_in_milliseconds = max_wait_time_in_seconds * 1000 self.current_retry_attempt_count = 0 self.cummulative_wait_time_in_milliseconds = 0 def ShouldRetry(self, exception): """Returns true if should retry based on the passed-in exception. :param (errors.HTTPFailure instance) exception: :rtype: boolean """ if self.current_retry_attempt_count < self._max_retry_attempt_count: self.current_retry_attempt_count += 1 self.retry_after_in_milliseconds = 0 if self._fixed_retry_interval_in_milliseconds: self.retry_after_in_milliseconds = self._fixed_retry_interval_in_milliseconds elif http_constants.HttpHeaders.RetryAfterInMilliseconds in exception.headers: self.retry_after_in_milliseconds = int(exception.headers[http_constants.HttpHeaders.RetryAfterInMilliseconds]) if self.cummulative_wait_time_in_milliseconds < self._max_wait_time_in_milliseconds: self.cummulative_wait_time_in_milliseconds += self.retry_after_in_milliseconds return True return False azure-cosmos-python-3.1.1/azure/cosmos/retry_options.py000066400000000000000000000045211352206500100234010ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Class for retry options in the Azure Cosmos database service. """ class RetryOptions(object): """The retry options to be applied to all requests when retrying :ivar int MaxRetryAttemptCount: Max number of retries to be performed for a request. Default value 9. :ivar int FixedRetryIntervalInMilliseconds: Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned as part of the response. :ivar int MaxWaitTimeInSeconds: Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds. """ def __init__(self, max_retry_attempt_count = 9, fixed_retry_interval_in_milliseconds = None, max_wait_time_in_seconds = 30): self._max_retry_attempt_count = max_retry_attempt_count self._fixed_retry_interval_in_milliseconds = fixed_retry_interval_in_milliseconds self._max_wait_time_in_seconds = max_wait_time_in_seconds @property def MaxRetryAttemptCount(self): return self._max_retry_attempt_count @property def FixedRetryIntervalInMilliseconds(self): return self._fixed_retry_interval_in_milliseconds @property def MaxWaitTimeInSeconds(self): return self._max_wait_time_in_secondsazure-cosmos-python-3.1.1/azure/cosmos/retry_utility.py000066400000000000000000000131661352206500100234160ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal methods for executing functions in the Azure Cosmos database service. """ import time import azure.cosmos.errors as errors import azure.cosmos.endpoint_discovery_retry_policy as endpoint_discovery_retry_policy import azure.cosmos.resource_throttle_retry_policy as resource_throttle_retry_policy import azure.cosmos.default_retry_policy as default_retry_policy import azure.cosmos.session_retry_policy as session_retry_policy from azure.cosmos.http_constants import HttpHeaders, StatusCodes, SubStatusCodes def _Execute(client, global_endpoint_manager, function, *args, **kwargs): """Exectutes the function with passed parameters applying all retry policies :param object client: Document client instance :param object global_endpoint_manager: Instance of _GlobalEndpointManager class :param function function: Function to be called wrapped with retries :param (non-keyworded, variable number of arguments list) *args: :param (keyworded, variable number of arguments list) **kwargs: """ # instantiate all retry policies here to be applied for each request execution endpointDiscovery_retry_policy = endpoint_discovery_retry_policy._EndpointDiscoveryRetryPolicy(client.connection_policy, global_endpoint_manager, *args) resourceThrottle_retry_policy = resource_throttle_retry_policy._ResourceThrottleRetryPolicy(client.connection_policy.RetryOptions.MaxRetryAttemptCount, client.connection_policy.RetryOptions.FixedRetryIntervalInMilliseconds, client.connection_policy.RetryOptions.MaxWaitTimeInSeconds) defaultRetry_policy = default_retry_policy._DefaultRetryPolicy(*args) sessionRetry_policy = session_retry_policy._SessionRetryPolicy(client.connection_policy.EnableEndpointDiscovery, global_endpoint_manager, *args) while True: try: if args: result = _ExecuteFunction(function, global_endpoint_manager, *args, **kwargs) else: result = _ExecuteFunction(function, *args, **kwargs) if not client.last_response_headers: client.last_response_headers = {} # setting the throttle related response headers before returning the result client.last_response_headers[HttpHeaders.ThrottleRetryCount] = resourceThrottle_retry_policy.current_retry_attempt_count client.last_response_headers[HttpHeaders.ThrottleRetryWaitTimeInMs] = resourceThrottle_retry_policy.cummulative_wait_time_in_milliseconds return result except errors.HTTPFailure as e: retry_policy = None if (e.status_code == StatusCodes.FORBIDDEN and e.sub_status == SubStatusCodes.WRITE_FORBIDDEN): retry_policy = endpointDiscovery_retry_policy elif e.status_code == StatusCodes.TOO_MANY_REQUESTS: retry_policy = resourceThrottle_retry_policy elif e.status_code == StatusCodes.NOT_FOUND and e.sub_status and e.sub_status == SubStatusCodes.READ_SESSION_NOTAVAILABLE: retry_policy = sessionRetry_policy else: retry_policy = defaultRetry_policy # If none of the retry policies applies or there is no retry needed, set the throttle related response hedaers and # re-throw the exception back # arg[0] is the request. It needs to be modified for write forbidden exception if not (retry_policy.ShouldRetry(e)): if not client.last_response_headers: client.last_response_headers = {} client.last_response_headers[HttpHeaders.ThrottleRetryCount] = resourceThrottle_retry_policy.current_retry_attempt_count client.last_response_headers[HttpHeaders.ThrottleRetryWaitTimeInMs] = resourceThrottle_retry_policy.cummulative_wait_time_in_milliseconds if len(args) > 0 and args[0].should_clear_session_token_on_session_read_failure: client.session.clear_session_token(client.last_response_headers) raise else: # Wait for retry_after_in_milliseconds time before the next retry time.sleep(retry_policy.retry_after_in_milliseconds / 1000.0) def _ExecuteFunction(function, *args, **kwargs): """ Stub method so that it can be used for mocking purposes as well. """ return function(*args, **kwargs)azure-cosmos-python-3.1.1/azure/cosmos/routing/000077500000000000000000000000001352206500100215745ustar00rootroot00000000000000azure-cosmos-python-3.1.1/azure/cosmos/routing/__init__.py000066400000000000000000000021171352206500100237060ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE.azure-cosmos-python-3.1.1/azure/cosmos/routing/collection_routing_map.py000066400000000000000000000165171352206500100267170ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for collection routing map implementation in the Azure Cosmos database service. """ import bisect from azure.cosmos.routing import routing_range from azure.cosmos.routing.routing_range import _PartitionKeyRange from six.moves import xrange class _CollectionRoutingMap(object): """Stores partition key ranges in an efficient way with some additional information and provides convenience methods for working with set of ranges. """ MinimumInclusiveEffectivePartitionKey = "" MaximumExclusiveEffectivePartitionKey = "FF" def __init__(self, range_by_id, range_by_info, ordered_partition_key_ranges, ordered_partition_info, collection_unique_id): self._rangeById = range_by_id self._rangeByInfo = range_by_info self._orderedPartitionKeyRanges = ordered_partition_key_ranges self._orderedRanges = [routing_range._Range(pkr[_PartitionKeyRange.MinInclusive], pkr[_PartitionKeyRange.MaxExclusive], True, False) for pkr in ordered_partition_key_ranges] self._orderedPartitionInfo = ordered_partition_info self._collectionUniqueId = collection_unique_id @classmethod def CompleteRoutingMap(cls, partition_key_range_info_tupple_list, collection_unique_id): rangeById = {} rangeByInfo = {} sortedRanges = [] for r in partition_key_range_info_tupple_list: rangeById[r[0][_PartitionKeyRange.Id]] = r rangeByInfo[r[1]] = r[0] sortedRanges.append(r) sortedRanges.sort(key = lambda r: r[0][_PartitionKeyRange.MinInclusive]) partitionKeyOrderedRange = [r[0] for r in sortedRanges] orderedPartitionInfo = [r[1] for r in sortedRanges] if not _CollectionRoutingMap.is_complete_set_of_range(partitionKeyOrderedRange): return None return cls(rangeById, rangeByInfo, partitionKeyOrderedRange, orderedPartitionInfo, collection_unique_id) def get_ordered_partition_key_ranges(self): """Gets the ordered partition key ranges :return: Ordered list of partition key ranges. :rtype: list """ return self._orderedPartitionKeyRanges def get_range_by_effective_partition_key(self, effective_partition_key_value): """Gets the range containing the given partition key :param str effective_partition_key_value: The partition key value. :return: The partition key range. :rtype: dict """ if _CollectionRoutingMap.MinimumInclusiveEffectivePartitionKey == effective_partition_key_value: return self._orderedPartitionKeyRanges[0] if _CollectionRoutingMap.MaximumExclusiveEffectivePartitionKey == effective_partition_key_value: return None sortedLow = [(r.min, not r.isMinInclusive) for r in self._orderedRanges] index = bisect.bisect_right(sortedLow, (effective_partition_key_value, True)) if (index > 0): index = index -1 return self._orderedPartitionKeyRanges[index] def get_range_by_partition_key_range_id(self, partition_key_range_id): """Gets the partition key range given the partition key range id :param str partition_key_range_id: The partition key range id. :return: The partition key range. :rtype: dict """ t = self._rangeById.get(partition_key_range_id) if t is None: return None return t[0] def get_overlapping_ranges(self, provided_partition_key_ranges): """Gets the partition key ranges overlapping the provided ranges :param list provided_partition_key_ranges: List of partition key ranges. :return: List of partition key ranges, where each is a dict. :rtype: list """ if isinstance(provided_partition_key_ranges, routing_range._Range): return self.get_overlapping_ranges([provided_partition_key_ranges]) minToPartitionRange = {} sortedLow = [(r.min, not r.isMinInclusive) for r in self._orderedRanges] sortedHigh = [(r.max, r.isMaxInclusive) for r in self._orderedRanges] for providedRange in provided_partition_key_ranges: minIndex = bisect.bisect_right(sortedLow, (providedRange.min, not providedRange.isMinInclusive)) if minIndex > 0: minIndex = minIndex - 1 maxIndex = bisect.bisect_left(sortedHigh, (providedRange.max, providedRange.isMaxInclusive)) if maxIndex >= len(sortedHigh): maxIndex = maxIndex - 1 for i in xrange(minIndex, maxIndex + 1): if routing_range._Range.overlaps(self._orderedRanges[i], providedRange): minToPartitionRange[self._orderedPartitionKeyRanges[i][_PartitionKeyRange.MinInclusive]] = self._orderedPartitionKeyRanges[i] overlapping_partition_key_ranges = list(minToPartitionRange.values()) def getKey(r): return r[_PartitionKeyRange.MinInclusive] overlapping_partition_key_ranges.sort(key = getKey) return overlapping_partition_key_ranges @staticmethod def is_complete_set_of_range(ordered_partition_key_range_list): isComplete = False if len(ordered_partition_key_range_list): firstRange = ordered_partition_key_range_list[0] lastRange = ordered_partition_key_range_list[-1] isComplete = (firstRange[_PartitionKeyRange.MinInclusive] == _CollectionRoutingMap.MinimumInclusiveEffectivePartitionKey) isComplete &= (lastRange[_PartitionKeyRange.MaxExclusive] == _CollectionRoutingMap.MaximumExclusiveEffectivePartitionKey) for i in range(1, len(ordered_partition_key_range_list)): previousRange = ordered_partition_key_range_list[i - 1] currentRange = ordered_partition_key_range_list[i] isComplete &= previousRange[_PartitionKeyRange.MaxExclusive] == currentRange[_PartitionKeyRange.MinInclusive] if not isComplete: if previousRange[_PartitionKeyRange.MaxExclusive] > currentRange[_PartitionKeyRange.MinInclusive]: raise ValueError("Ranges overlap") break return isComplete azure-cosmos-python-3.1.1/azure/cosmos/routing/routing_map_provider.py000066400000000000000000000203211352206500100264020ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for partition key range cache implementation in the Azure Cosmos database service. """ import azure.cosmos.base as base from azure.cosmos.routing.collection_routing_map import _CollectionRoutingMap import azure.cosmos.routing.routing_range as routing_range from azure.cosmos.routing.routing_range import _PartitionKeyRange class _PartitionKeyRangeCache(object): ''' _PartitionKeyRangeCache provides list of effective partition key ranges for a collection. This implementation loads and caches the collection routing map per collection on demand. ''' def __init__(self, client): ''' Constructor ''' self._documentClient = client # keeps the cached collection routing map by collection id self._collection_routing_map_by_item = {} def get_overlapping_ranges(self, collection_link, partition_key_ranges): ''' Given a partition key range and a collection, returns the list of overlapping partition key ranges :param str collection_link: The name of the collection. :param list partition_key_range: List of partition key range. :return: List of overlapping partition key ranges. :rtype: list ''' cl = self._documentClient collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) collection_routing_map = self._collection_routing_map_by_item.get(collection_id) if collection_routing_map is None: collection_pk_ranges = list(cl._ReadPartitionKeyRanges(collection_link)) # for large collections, a split may complete between the read partition key ranges query page responses, # causing the partitionKeyRanges to have both the children ranges and their parents. Therefore, we need # to discard the parent ranges to have a valid routing map. collection_pk_ranges = _PartitionKeyRangeCache._discard_parent_ranges(collection_pk_ranges) collection_routing_map = _CollectionRoutingMap.CompleteRoutingMap([(r, True) for r in collection_pk_ranges], collection_id) self._collection_routing_map_by_item[collection_id] = collection_routing_map return collection_routing_map.get_overlapping_ranges(partition_key_ranges) @staticmethod def _discard_parent_ranges(partitionKeyRanges): parentIds = set() for r in partitionKeyRanges: if isinstance(r, dict) and _PartitionKeyRange.Parents in r: for parentId in r[_PartitionKeyRange.Parents]: parentIds.add(parentId) return (r for r in partitionKeyRanges if r[_PartitionKeyRange.Id] not in parentIds) class _SmartRoutingMapProvider(_PartitionKeyRangeCache): """ Efficiently uses PartitionKeyRangeCach and minimizes the unnecessary invocation of _CollectionRoutingMap.get_overlapping_ranges() """ def __init__(self, client): super(_SmartRoutingMapProvider, self).__init__(client) def _second_range_is_after_first_range(self, range1, range2): if range1.max > range2.min: ##r.min < #previous_r.max return False else: if (range2.min == range2.min and range1.isMaxInclusive and range2.isMinInclusive): # the inclusive ending endpoint of previous_r is the same as the inclusive beginning endpoint of r return False return True def _is_sorted_and_non_overlapping(self, ranges): for idx, r in list(enumerate(ranges))[1:]: previous_r = ranges[idx-1] if not self._second_range_is_after_first_range(previous_r, r): return False return True def _subtract_range(self, r, partition_key_range): """ Evaluates and returns r - partition_key_range :param dict partition_key_range: Partition key range. :param routing_range._Range r: query range. :return: The subtract r - partition_key_range. :rtype: routing_range._Range """ left = max(partition_key_range[routing_range._PartitionKeyRange.MaxExclusive], r.min) if left == r.min: leftInclusive = r.isMinInclusive else: leftInclusive = False queryRange = routing_range._Range(left, r.max, leftInclusive, r.isMaxInclusive) return queryRange def get_overlapping_ranges(self, collection_link, sorted_ranges): ''' Given the sorted ranges and a collection, Returns the list of overlapping partition key ranges :param str collection_link: The collection link. :param (list of routing_range._Range) sorted_ranges: The sorted list of non-overlapping ranges. :return: List of partition key ranges. :rtype: list of dict :raises ValueError: If two ranges in sorted_ranges overlap or if the list is not sorted ''' # validate if the list is non-overlapping and sorted if not self._is_sorted_and_non_overlapping(sorted_ranges): raise ValueError("the list of ranges is not a non-overlapping sorted ranges") target_partition_key_ranges = [] it = iter(sorted_ranges) try: currentProvidedRange = next(it) while True: if (currentProvidedRange.isEmpty()): # skip and go to the next item\ currentProvidedRange = next(it) continue if len(target_partition_key_ranges): queryRange = self._subtract_range(currentProvidedRange, target_partition_key_ranges[-1]) else: queryRange = currentProvidedRange overlappingRanges = _PartitionKeyRangeCache.get_overlapping_ranges(self, collection_link, queryRange) assert len(overlappingRanges), ("code bug: returned overlapping ranges for queryRange {} is empty".format(queryRange)) target_partition_key_ranges.extend(overlappingRanges) lastKnownTargetRange = routing_range._Range.PartitionKeyRangeToRange(target_partition_key_ranges[-1]) # the overlapping ranges must contain the requested range assert currentProvidedRange.max <= lastKnownTargetRange.max, "code bug: returned overlapping ranges {} does not contain the requested range {}".format(overlappingRanges, queryRange) # the current range is contained in target_partition_key_ranges just move forward currentProvidedRange = next(it) while currentProvidedRange.max <= lastKnownTargetRange.max: # the current range is covered too. just move forward currentProvidedRange = next(it) except StopIteration: # when the iteration is exhausted we get here. There is nothing else to be done pass return target_partition_key_ranges azure-cosmos-python-3.1.1/azure/cosmos/routing/routing_range.py000066400000000000000000000103661352206500100250170ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for partition key range implementation in the Azure Cosmos database service. """ class _PartitionKeyRange(object): """Partition Key Range Constants""" MinInclusive = 'minInclusive' MaxExclusive = 'maxExclusive' Id = 'id' Parents = 'parents' class _Range(object): """description of class""" MinPath = 'min' MaxPath = 'max' IsMinInclusivePath = 'isMinInclusive' IsMaxInclusivePath = 'isMaxInclusive' def __init__(self, range_min, range_max, isMinInclusive, isMaxInclusive): if range_min is None: raise ValueError("min is missing") if range_max is None: raise ValueError("max is missing") self.min = range_min self.max = range_max self.isMinInclusive = isMinInclusive self.isMaxInclusive = isMaxInclusive def contains(self, value): minToValueRelation = self.min > value maxToValueRelation = self.max > value return ((self.isMinInclusive and minToValueRelation <= 0) or \ (not self.isMinInclusive and minToValueRelation < 0)) \ and ((self.isMaxInclusive and maxToValueRelation >= 0) \ or (not self.isMaxInclusive and maxToValueRelation > 0)) @classmethod def PartitionKeyRangeToRange(cls, partition_key_range): self = cls(partition_key_range[_PartitionKeyRange.MinInclusive], partition_key_range[_PartitionKeyRange.MaxExclusive], True, False) return self @classmethod def ParseFromDict(cls, range_as_dict): self = cls(range_as_dict[_Range.MinPath], range_as_dict[_Range.MaxPath], range_as_dict[_Range.IsMinInclusivePath], range_as_dict[_Range.IsMaxInclusivePath]) return self def isSingleValue(self): return self.isMinInclusive and self.isMaxInclusive and self.min == self.max def isEmpty(self): return (not (self.isMinInclusive and self.isMaxInclusive)) and self.min == self.max def __hash__(self): return hash((self.min, self.max, self.isMinInclusive, self.isMaxInclusive)) def __str__(self): return (('[' if self.isMinInclusive else '(') + str(self.min) + ',' + str(self.max) + (']' if self.isMaxInclusive else ')')) def __eq__(self, other): return (self.min == other.min) and (self.max == other.max) \ and (self.isMinInclusive == other.isMinInclusive) \ and (self.isMaxInclusive == other.isMaxInclusive) @staticmethod def _compare_helper(a,b): # python 3 compatible return (a > b) - (a < b) @staticmethod def overlaps(range1, range2): if range1 is None or range2 is None: return False if range1.isEmpty() or range2.isEmpty(): return False cmp1 = _Range._compare_helper(range1.min, range2.max) cmp2 = _Range._compare_helper(range2.min, range1.max) if (cmp1 <= 0 or cmp2 <= 0): if ((cmp1 == 0 and not(range1.isMinInclusive and range2.isMaxInclusive)) or (cmp2 == 0 and not(range2.isMinInclusive and range1.isMaxInclusive))): return False return True return False azure-cosmos-python-3.1.1/azure/cosmos/runtime_constants.py000066400000000000000000000031471352206500100242430ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Runtime Constants in the Azure Cosmos database service. """ class MediaTypes: """Constants of media types. http://www.iana.org/assignments/media-types/media-types.xhtml """ Any = '*/*' ImageJpeg = 'image/jpeg' ImagePng = 'image/png' JavaScript = 'application/x-javascript' Json = 'application/json' OctetStream = 'application/octet-stream' QueryJson = 'application/query+json' SQL = 'application/sql' TextHtml = 'text/html' TextPlain = 'text/plain' Xml = 'application/xml'azure-cosmos-python-3.1.1/azure/cosmos/session.py000066400000000000000000000225741352206500100221540ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Session Consistency Tracking in the Azure Cosmos database service. """ import sys, traceback import threading import azure.cosmos.base as base import azure.cosmos.http_constants as http_constants from azure.cosmos.vector_session_token import VectorSessionToken from azure.cosmos.errors import HTTPFailure class SessionContainer(object): def __init__(self): self.collection_name_to_rid = {} self.rid_to_session_token = {} self.session_lock = threading.RLock() def get_session_token(self, resource_path): """ Get Session Token for collection_link :param str resource_path: Self link / path to the resource :return: Session Token dictionary for the collection_id :rtype: dict """ with self.session_lock: is_name_based = base.IsNameBased(resource_path) collection_rid = '' session_token = '' try: if is_name_based: # get the collection name collection_name = base.GetItemContainerLink(resource_path) collection_rid = self.collection_name_to_rid[collection_name] else: collection_rid = base.GetItemContainerLink(resource_path) if collection_rid in self.rid_to_session_token: token_dict = self.rid_to_session_token[collection_rid] session_token_list = [] for key in token_dict.keys(): session_token_list.append("{0}:{1}".format(key, token_dict[key].convert_to_string())) session_token = ','.join(session_token_list) return session_token else: # return empty token if not found return '' except: return '' def set_session_token(self, response_result, response_headers): """ Session token must only be updated from response of requests that successfully mutate resource on the server side (write, replace, delete etc) :param dict response_result: :param dict response_headers: :return: - None """ ''' there are two pieces of information that we need to update session token- self link which has the rid representation of the resource, and x-ms-alt-content-path which is the string representation of the resource''' with self.session_lock: collection_rid = '' collection_name = '' try: self_link = response_result['_self'] ''' extract alternate content path from the response_headers (only document level resource updates will have this), and if not present, then we can assume that we don't have to update session token for this request''' alt_content_path = '' alt_content_path_key = http_constants.HttpHeaders.AlternateContentPath response_result_id_key = u'id' response_result_id = None if alt_content_path_key in response_headers: alt_content_path = response_headers[http_constants.HttpHeaders.AlternateContentPath] response_result_id = response_result[response_result_id_key] else: return collection_rid, collection_name = base.GetItemContainerInfo(self_link, alt_content_path, response_result_id) except ValueError: return except: exc_type, exc_value, exc_traceback = sys.exc_info() traceback.print_exception(exc_type, exc_value, exc_traceback, limit=2, file=sys.stdout) return if collection_name in self.collection_name_to_rid: ''' check if the rid for the collection name has changed this means that potentially, the collection was deleted and recreated ''' existing_rid = self.collection_name_to_rid[collection_name] if (collection_rid != existing_rid): ''' flush the session tokens for the old rid, and update the new rid into the collection name to rid map. ''' self.rid_to_session_token[existing_rid] = {} self.collection_name_to_rid[collection_name] = collection_rid # parse session token parsed_tokens = self.parse_session_token(response_headers) # update session token in collection rid to session token map if collection_rid in self.rid_to_session_token: ''' we need to update the session tokens for 'this' collection ''' for id in parsed_tokens: old_session_token = self.rid_to_session_token[collection_rid][id] if id in self.rid_to_session_token[collection_rid] else None if not old_session_token: self.rid_to_session_token[collection_rid][id] = parsed_tokens[id] else: self.rid_to_session_token[collection_rid][id] = parsed_tokens[id].merge(old_session_token) self.collection_name_to_rid[collection_name] = collection_rid else: self.rid_to_session_token[collection_rid] = parsed_tokens self.collection_name_to_rid[collection_name] = collection_rid def clear_session_token(self, response_headers): with self.session_lock: collection_rid = '' alt_content_path = '' alt_content_path_key = http_constants.HttpHeaders.AlternateContentPath if alt_content_path_key in response_headers: alt_content_path = response_headers[http_constants.HttpHeaders.AlternateContentPath] if alt_content_path in self.collection_name_to_rid: collection_rid = self.collection_name_to_rid[alt_content_path] del self.collection_name_to_rid[alt_content_path] del self.rid_to_session_token[collection_rid] @staticmethod def parse_session_token(response_headers): """ Extracts session token from response headers and parses :param dict response_headers: :return: A dictionary of partition id to session lsn for given collection :rtype: dict """ # extract session token from response header session_token = '' if http_constants.HttpHeaders.SessionToken in response_headers: session_token = response_headers[http_constants.HttpHeaders.SessionToken] id_to_sessionlsn = {} if session_token is not '': ''' extract id, lsn from the token. For p-collection, the token will be a concatenation of pairs for each collection''' token_pairs = session_token.split(',') for token_pair in token_pairs: tokens = token_pair.split(':') if (len(tokens) == 2): id = tokens[0] sessionToken = VectorSessionToken.create(tokens[1]) if sessionToken is None: raise HTTPFailure(http_constants.StatusCodes.INTERNAL_SERVER_ERROR, "Could not parse the received session token: %s" % tokens[1]) id_to_sessionlsn[id] = sessionToken return id_to_sessionlsn class Session: """ State of a Azure Cosmos session. This session object can be shared across clients within the same process """ def __init__(self, url_connection): self.url_connection = url_connection self.session_container = SessionContainer() #include creation time, and some other stats def clear_session_token(self, response_headers): self.session_container.clear_session_token(response_headers) def update_session(self, response_result, response_headers): self.session_container.set_session_token(response_result, response_headers) def get_session_token(self, resource_path): return self.session_container.get_session_token(resource_path)azure-cosmos-python-3.1.1/azure/cosmos/session_retry_policy.py000066400000000000000000000135371352206500100247570ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2018 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal class for session read/write unavailable retry policy implementation in the Azure Cosmos database service. """ import logging from azure.cosmos.documents import _OperationType logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) log_formatter = logging.Formatter('%(levelname)s:%(message)s') log_handler = logging.StreamHandler() log_handler.setFormatter(log_formatter) logger.addHandler(log_handler) class _SessionRetryPolicy(object): """The session retry policy used to handle read/write session unavailability. """ Max_retry_attempt_count = 1 Retry_after_in_milliseconds = 0 def __init__(self, endpoint_discovery_enable, global_endpoint_manager, *args): self.global_endpoint_manager = global_endpoint_manager self._max_retry_attempt_count = _SessionRetryPolicy.Max_retry_attempt_count self.session_token_retry_count = 0 self.retry_after_in_milliseconds = _SessionRetryPolicy.Retry_after_in_milliseconds self.endpoint_discovery_enable = endpoint_discovery_enable self.request = args[0] if args else None if self.request: self.can_use_multiple_write_locations = self.global_endpoint_manager.can_use_multiple_write_locations(self.request) # clear previous location-based routing directive self.request.clear_route_to_location() # Resolve the endpoint for the request and pin the resolution to the resolved endpoint # This enables marking the endpoint unavailability on endpoint failover/unreachability self.location_endpoint = self.global_endpoint_manager.resolve_service_endpoint(self.request) self.request.route_to_location(self.location_endpoint) def ShouldRetry(self, exception): """Returns true if should retry based on the passed-in exception. :param (errors.HTTPFailure instance) exception: :rtype: boolean """ self.session_token_retry_count += 1 # clear previous location-based routing directive self.request.clear_route_to_location() if not self.endpoint_discovery_enable: # if endpoint discovery is disabled, the request cannot be retried anywhere else return False else: if self.can_use_multiple_write_locations: if _OperationType.IsReadOnlyOperation(self.request.operation_type): endpoints = self.global_endpoint_manager.get_ordered_read_endpoints() else: endpoints = self.global_endpoint_manager.get_ordered_write_endpoints() if self.session_token_retry_count > len(endpoints): # When use multiple write locations is true and the request has been tried # on all locations, then don't retry the request return False else: # set location-based routing directive based on request retry context self.request.route_to_location_with_preferred_location_flag(self.session_token_retry_count - 1, self.session_token_retry_count > self._max_retry_attempt_count) self.request.should_clear_session_token_on_session_read_failure = self.session_token_retry_count == len(endpoints) # clear on last attempt # Resolve the endpoint for the request and pin the resolution to the resolved endpoint # This enables marking the endpoint unavailability on endpoint failover/unreachability self.location_endpoint = self.global_endpoint_manager.resolve_service_endpoint(self.request) self.request.route_to_location(self.location_endpoint) return True else: if self.session_token_retry_count > self._max_retry_attempt_count: # When cannot use multiple write locations, then don't retry the request if # we have already tried this request on the write location return False else: # set location-based routing directive based on request retry context self.request.route_to_location_with_preferred_location_flag(self.session_token_retry_count - 1, False) self.request.should_clear_session_token_on_session_read_failure = True # Resolve the endpoint for the request and pin the resolution to the resolved endpoint # This enables marking the endpoint unavailability on endpoint failover/unreachability self.location_endpoint = self.global_endpoint_manager.resolve_service_endpoint(self.request) self.request.route_to_location(self.location_endpoint) return True azure-cosmos-python-3.1.1/azure/cosmos/synchronized_request.py000066400000000000000000000206601352206500100247520ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Synchronized request in the Azure Cosmos database service. """ import json from six.moves.urllib.parse import urlparse, urlencode import six import azure.cosmos.documents as documents import azure.cosmos.errors as errors import azure.cosmos.http_constants as http_constants import azure.cosmos.retry_utility as retry_utility def _IsReadableStream(obj): """Checks whether obj is a file-like readable stream. :rtype: boolean """ if (hasattr(obj, 'read') and callable(getattr(obj, 'read'))): return True return False def _RequestBodyFromData(data): """Gets request body from data. When `data` is dict and list into unicode string; otherwise return `data` without making any change. :param (str, unicode, file-like stream object, dict, list or None) data: :rtype: str, unicode, file-like stream object, or None """ if isinstance(data, six.string_types) or _IsReadableStream(data): return data elif isinstance(data, (dict, list, tuple)): json_dumped = json.dumps(data, separators=(',',':')) if six.PY2: return json_dumped.decode('utf-8') else: return json_dumped return None def _Request(global_endpoint_manager, request, connection_policy, requests_session, path, request_options, request_body): """Makes one http request using the requests module. :param _GlobalEndpointManager global_endpoint_manager: :param dict request: contains the resourceType, operationType, endpointOverride, useWriteEndpoint, useAlternateWriteEndpoint information :param documents.ConnectionPolicy connection_policy: :param requests.Session requests_session: Session object in requests module :param str resource_url: The url for the resource :param dict request_options: :param str request_body: Unicode or None :return: tuple of (result, headers) :rtype: tuple of (dict, dict) """ is_media = request_options['path'].find('media') > -1 is_media_stream = is_media and connection_policy.MediaReadMode == documents.MediaReadMode.Streamed connection_timeout = (connection_policy.MediaRequestTimeout if is_media else connection_policy.RequestTimeout) # Every request tries to perform a refresh global_endpoint_manager.refresh_endpoint_list(None) if (request.endpoint_override): base_url = request.endpoint_override else: base_url = global_endpoint_manager.resolve_service_endpoint(request) if path: resource_url = base_url + path else: resource_url = base_url parse_result = urlparse(resource_url) # The requests library now expects header values to be strings only starting 2.11, # and will raise an error on validation if they are not, so casting all header values to strings. request_options['headers'] = { header: str(value) for header, value in request_options['headers'].items() } # We are disabling the SSL verification for local emulator(localhost/127.0.0.1) or if the user # has explicitly specified to disable SSL verification. is_ssl_enabled = (parse_result.hostname != 'localhost' and parse_result.hostname != '127.0.0.1' and not connection_policy.DisableSSLVerification) if connection_policy.SSLConfiguration: ca_certs = connection_policy.SSLConfiguration.SSLCaCerts cert_files = (connection_policy.SSLConfiguration.SSLCertFile, connection_policy.SSLConfiguration.SSLKeyFile) response = requests_session.request(request_options['method'], resource_url, data = request_body, headers = request_options['headers'], timeout = connection_timeout / 1000.0, stream = is_media_stream, verify = ca_certs, cert = cert_files) else: response = requests_session.request(request_options['method'], resource_url, data = request_body, headers = request_options['headers'], timeout = connection_timeout / 1000.0, stream = is_media_stream, # If SSL is disabled, verify = false verify = is_ssl_enabled) headers = dict(response.headers) # In case of media stream response, return the response to the user and the user # will need to handle reading the response. if is_media_stream: return (response.raw, headers) data = response.content if not six.PY2: # python 3 compatible: convert data from byte to unicode string data = data.decode('utf-8') if response.status_code >= 400: raise errors.HTTPFailure(response.status_code, data, headers) result = None if is_media: result = data else: if len(data) > 0: try: result = json.loads(data) except: raise errors.JSONParseFailure(data) return (result, headers) def SynchronizedRequest(client, request, global_endpoint_manager, connection_policy, requests_session, method, path, request_data, query_params, headers): """Performs one synchronized http request according to the parameters. :param object client: Document client instance :param dict request: :param _GlobalEndpointManager global_endpoint_manager: :param documents.ConnectionPolicy connection_policy: :param requests.Session requests_session: Session object in requests module :param str method: :param str path: :param (str, unicode, file-like stream object, dict, list or None) request_data: :param dict query_params: :param dict headers: :return: tuple of (result, headers) :rtype: tuple of (dict dict) """ request_body = None if request_data: request_body = _RequestBodyFromData(request_data) if not request_body: raise errors.UnexpectedDataType( 'parameter data must be a JSON object, string or' + ' readable stream.') request_options = {} request_options['path'] = path request_options['method'] = method if query_params: request_options['path'] += '?' + urlencode(query_params) request_options['headers'] = headers if request_body and (type(request_body) is str or type(request_body) is six.text_type): request_options['headers'][http_constants.HttpHeaders.ContentLength] = ( len(request_body)) elif request_body is None: request_options['headers'][http_constants.HttpHeaders.ContentLength] = 0 # Pass _Request function with it's parameters to retry_utility's Execute method that wraps the call with retries return retry_utility._Execute(client, global_endpoint_manager, _Request, request, connection_policy, requests_session, path, request_options, request_body) azure-cosmos-python-3.1.1/azure/cosmos/utils.py000066400000000000000000000034721352206500100216250ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Internal Helper functions in the Azure Cosmos database service. """ import platform import re as re import azure.cosmos.http_constants as http_constants def _get_user_agent(): os_name = _safe_user_agent_header(platform.system()) os_version = _safe_user_agent_header(platform.release()) python_version = _safe_user_agent_header(platform.python_version()) user_agent = "{}/{} Python/{} {}/{}".format(os_name, os_version, python_version, http_constants.Versions.SDKName, http_constants.Versions.SDKVersion) return user_agent def _safe_user_agent_header(s): if s is None: s = "unknown" # remove all white spaces s = re.sub(r"\s+", '', s) if not s: s = "unknown" return sazure-cosmos-python-3.1.1/azure/cosmos/vector_session_token.py000066400000000000000000000141711352206500100247300ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2018 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Session Consistency Tracking in the Azure Cosmos database service. """ import azure.cosmos.errors as errors import azure.cosmos.base as base from azure.cosmos.http_constants import StatusCodes class VectorSessionToken(object): segment_separator = '#' region_progress_separator = "=" def __init__(self, version, global_lsn, local_lsn_by_region, session_token=None): self.version = version self.global_lsn = global_lsn self.local_lsn_by_region = local_lsn_by_region self.session_token = session_token if self.session_token == None: region_and_local_lsn = [] for key in self.local_lsn_by_region: region_and_local_lsn.append(str(key) + self.region_progress_separator + str(self.local_lsn_by_region[key])) region_progress = self.segment_separator.join(region_and_local_lsn) if not region_progress: self.session_token = "%s%s%s" % (self.version, self.segment_separator, self.global_lsn) else: self.session_token = "%s%s%s%s%s" % (self.version, self.segment_separator, self.global_lsn, self.segment_separator, region_progress) @classmethod def create(cls, session_token): """ Parses session token and creates the vector session token :param str session_token: :return: A Vector session Token :rtype: VectorSessionToken """ version = None global_lsn = None local_lsn_by_region = {} if not session_token: return None segments = session_token.split(cls.segment_separator) if len(segments) < 2: return None try: version = int(segments[0]) except ValueError as _: return None try: global_lsn = int(segments[1]) except ValueError as _: return None for i in range(2, len(segments)): region_segment = segments[i] region_id_with_lsn = region_segment.split(cls.region_progress_separator) if len(region_id_with_lsn) != 2: return None try: region_id = int(region_id_with_lsn[0]) local_lsn = int(region_id_with_lsn[1]) except ValueError as _: return None local_lsn_by_region[region_id] = local_lsn return VectorSessionToken(version, global_lsn, local_lsn_by_region, session_token) def equals(self, other): if other is None: return False else: return self.version == other.version and self.global_lsn == other.global_lsn and self.are_region_progress_equal(other.local_lsn_by_region) def merge(self, other): if other is None: raise ValueError("Invalid Session Token (should not be None)") if self.version == other.version and len(self.local_lsn_by_region) != len(other.local_lsn_by_region): raise errors.CosmosError(Exception("Status Code: %s. Compared session tokens '%s' and '%s' have unexpected regions." % (StatusCodes.INTERNAL_SERVER_ERROR, self.session_token, other.session_token))) if self.version < other.version: session_token_with_lower_version = self session_token_with_higher_version = other else: session_token_with_lower_version = other session_token_with_higher_version = self highest_local_lsn_by_region = {} for key in session_token_with_higher_version.local_lsn_by_region: region_id = key local_lsn1 = session_token_with_higher_version.local_lsn_by_region[key] local_lsn2 = session_token_with_lower_version.local_lsn_by_region[region_id] if region_id in session_token_with_lower_version.local_lsn_by_region else None if local_lsn2 is not None: highest_local_lsn_by_region[region_id] = max(local_lsn1, local_lsn2) elif self.version == other.version: raise errors.CosmosError(Exception("Status Code: %s. Compared session tokens '%s' and '%s' have unexpected regions." % (StatusCodes.INTERNAL_SERVER_ERROR, self.session_token, other.session_token))) else: highest_local_lsn_by_region[region_id] = local_lsn1 return VectorSessionToken(max(self.version, other.version), max(self.global_lsn, other.global_lsn), highest_local_lsn_by_region) def convert_to_string(self): return self.session_token def are_region_progress_equal(self, other): if len(self.local_lsn_by_region) != len(other): return False for key in self.local_lsn_by_region: region_id = key local_lsn1 = self.local_lsn_by_region[region_id] local_lsn2 = other[region_id] if region_id in other else None if local_lsn2 is not None: if local_lsn1 != local_lsn2: return False return Trueazure-cosmos-python-3.1.1/changelog.md000066400000000000000000000115471352206500100177350ustar00rootroot00000000000000## Changes in 3.1.1 : ## - Bug fix in orderby queries to honor maxItemCount ## Changes in 3.1.0 : ## - Added support for picking up endpoint and key from environment variables ## Changes in 3.0.2 : ## - Added Support for MultiPolygon Datatype - Bug Fix in Session Read Retry Policy - Bug Fix for Incorrect padding issues while decoding base 64 strings ## Changes in 3.0.1 : ## - Bug fix in LocationCache - Bug fix endpoint retry logic - Fixed documentation ## Changes in 3.0.0 : ## - Multi-region write support added - Naming changes - DocumentClient to CosmosClient - Collection to Container - Document to Item - Package name updated to "azure-cosmos" - Namespace updated to "azure.cosmos" ## Changes in 2.3.3 : ## - Added support for proxy - Added support for reading change feed - Added support for collection quota headers - Bugfix for large session tokens issue - Bugfix for ReadMedia API - Bugfix in partition key range cache ## Changes in 2.3.2 : ## - Added support for default retries on connection issues. ## Changes in 2.3.1 : ## - Updated documentation to reference Azure Cosmos DB instead of Azure DocumentDB. ## Changes in 2.3.0 : ## - This SDK version requires the latest version of Azure Cosmos DB Emulator available for download from https://aka.ms/cosmosdb-emulator. ## Changes in 2.2.1 : ## - bugfix for aggregate dict - bugfix for trimming slashes in the resource link - tests for unicode encoding ## Changes in 2.2.0 : ## - Added support for Request Unit per Minute (RU/m) feature. - Added support for a new consistency level called ConsistentPrefix. ## Changes in 2.1.0 : ## - Added support for aggregation queries (COUNT, MIN, MAX, SUM, and AVG). - Added an option for disabling SSL verification when running against DocumentDB Emulator. - Removed the restriction of dependent requests module to be exactly 2.10.0. - Lowered minimum throughput on partitioned collections from 10,100 RU/s to 2500 RU/s. - Added support for enabling script logging during stored procedure execution. - REST API version bumped to '2017-01-19' with this release. ## Changes in 2.0.1 : ## - Made editorial changes to documentation comments. ## Changes in 2.0.0 : ## - Added support for Python 3.5. - Added support for connection pooling using the requests module. - Added support for session consistency. - Added support for TOP/ORDERBY queries for partitioned collections. ## Changes in 1.9.0 : ## - Added retry policy support for throttled requests. (Throttled requests receive a request rate too large exception, error code 429.) By default, DocumentDB retries nine times for each request when error code 429 is encountered, honoring the retryAfter time in the response header. A fixed retry interval time can now be set as part of the RetryOptions property on the ConnectionPolicy object if you want to ignore the retryAfter time returned by server between the retries. DocumentDB now waits for a maximum of 30 seconds for each request that is being throttled (irrespective of retry count) and returns the response with error code 429. This time can also be overriden in the RetryOptions property on ConnectionPolicy object. - DocumentDB now returns x-ms-throttle-retry-count and x-ms-throttle-retry-wait-time-ms as the response headers in every request to denote the throttle retry count and the cummulative time the request waited between the retries. - Removed the RetryPolicy class and the corresponding property (retry_policy) exposed on the document_client class and instead introduced a RetryOptions class exposing the RetryOptions property on ConnectionPolicy class that can be used to override some of the default retry options. ## Changes in 1.8.0 : ## - Added the support for geo-replicated database accounts. - Test fixes to move the global host and masterKey into the individual test classes. ## Changes in 1.7.0 : ## - Added the support for Time To Live(TTL) feature for documents. ## Changes in 1.6.1 : ## - Bug fixes related to server side partitioning to allow special characters in partitionkey path. ## Changes in 1.6.0 : ## - Added the support for server side partitioned collections feature. ## Changes in 1.5.0 : ## - Added Client-side sharding framework to the SDK. Implemented HashPartionResolver and RangePartitionResolver classes. ## Changes in 1.4.2 : ## - Implement Upsert. New UpsertXXX methods added to support Upsert feature. - Implement ID Based Routing. No public API changes, all changes internal. ## Changes in 1.3.0 : ## - Release skipped to bring version number in alignment with other SDKs ## Changes in 1.2.0 : ## - Supports GeoSpatial index. - Validates id property for all resources. Ids for resources cannot contain ?, /, #, \\, characters or end with a space. - Adds new header "index transformation progress" to ResourceResponse. ## Changes in 1.1.0 : ## - Implements V2 indexing policy ## Changes in 1.0.1 : ## - Supports proxy connection azure-cosmos-python-3.1.1/doc/000077500000000000000000000000001352206500100162215ustar00rootroot00000000000000azure-cosmos-python-3.1.1/doc/Makefile000066400000000000000000000152021352206500100176610ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/azure-cosmos.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/azure-cosmos.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/azure-cosmos" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/azure-cosmos" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." azure-cosmos-python-3.1.1/doc/__init__.py000066400000000000000000000000001352206500100203200ustar00rootroot00000000000000azure-cosmos-python-3.1.1/doc/conf.py000066400000000000000000000162351352206500100175270ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # azure-cosmos documentation build configuration file, created by # sphinx-quickstart on Fri Jun 27 15:42:45 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'azure-cosmos' copyright = u'2017, Microsoft' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '3.1.1' # The full version, including alpha/beta/rc tags. release = '3.1.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for extensions ---------------------------------------------------- autoclass_content = 'both' # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' html_theme_options = {'collapsiblesidebar': True} # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'azure-cosmosdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'azure-cosmos.tex', u'azure-cosmos Documentation', u'Microsoft', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True azure-cosmos-python-3.1.1/doc/index.rst000066400000000000000000000035271352206500100200710ustar00rootroot00000000000000.. azure-cosmos documentation master file, created by sphinx-quickstart on Fri Jun 27 15:42:45 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Azure Cosmos Python SDK ======================================== System Requirements: -------------------- The supported Python versions are 2.7, 3.3, 3.4 and 3.5. To download Python, please visit https://www.python.org/download/releases. Python Tools for Visual Studio is required when using Microsoft Visual Studio to develop Python applications. To download Python Tools for Visual Studio, please visit http://microsoft.github.io/PTVS. Installation: ------------- Method 1: 1. Download the Azure Cosmos Python SDK source from https://github.com/Azure/azure-cosmos-python which is needed to manage the Azure Cosmos database service. 2. Execute the following setup script in bash shell: .. code-block:: bash $ python setup.py install Method 2: 1. Install the Azure Cosmos Python SDK using pip. For more information on pip, please visit https://pypi.python.org/pypi/pip 2. Execute the following in bash shell: .. code-block:: bash $ pip install azure-cosmos To run tests: ------------- .. code-block:: bash $ python -m unittest discover -s .\test -p "*.py" If you use Microsoft Visual Studio, open the project file python.pyproj, and run all the tests in Test Explorer. To generate documentations: --------------------------- Install Sphinx: http://sphinx-doc.org/install.html .. code-block:: bash $ cd doc $ sphinx-apidoc -f -e -o api ..\azure $ make.bat html Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. toctree:: :hidden: api/azure api/modulesazure-cosmos-python-3.1.1/doc/make.bat000066400000000000000000000145071352206500100176350ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\azure-cosmos.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\azure-cosmos.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end azure-cosmos-python-3.1.1/python.pyproj000066400000000000000000000137171352206500100202530ustar00rootroot00000000000000 Debug 2.0 {ea429a23-2afb-4bf7-a345-28e2bd129564} . . . . 2.7 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) False {9a7a9026-48c1-4688-9d5d-e5699d47d074} $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets azure-cosmos-python-3.1.1/python.sln000066400000000000000000000014321352206500100175130ustar00rootroot00000000000000 Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "python", "python.pyproj", "{EA429A23-2AFB-4BF7-A345-28E2BD129564}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {EA429A23-2AFB-4BF7-A345-28E2BD129564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA429A23-2AFB-4BF7-A345-28E2BD129564}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal azure-cosmos-python-3.1.1/python_vs2017.pyproj000066400000000000000000000120411352206500100212620ustar00rootroot00000000000000 Debug 2.0 {ea429a23-2afb-4bf7-a345-28e2bd129564} . . . . 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) False Global|PythonCore|2.7 azure-cosmos-python-3.1.1/python_vs2017.sln000066400000000000000000000016471352206500100205450ustar00rootroot00000000000000 Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "python_vs2017", "python_vs2017.pyproj", "{EA429A23-2AFB-4BF7-A345-28E2BD129564}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {EA429A23-2AFB-4BF7-A345-28E2BD129564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA429A23-2AFB-4BF7-A345-28E2BD129564}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2A4705EC-6E91-4A5D-AB76-AA23575FA444} EndGlobalSection EndGlobal azure-cosmos-python-3.1.1/requirements.txt000066400000000000000000000000331352206500100207340ustar00rootroot00000000000000requests >=2.10.0 six >=1.6azure-cosmos-python-3.1.1/samples/000077500000000000000000000000001352206500100171205ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/ChangeFeedManagement/000077500000000000000000000000001352206500100230665ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/ChangeFeedManagement/ChangeFeedManagement.pyproj000066400000000000000000000031101352206500100302740ustar00rootroot00000000000000 Debug 2.0 976e1caa-d0a9-40aa-b4c2-1a0b4db71703 . Program.py ..\Shared . . ChangeFeedManagement ChangeFeedManagement true false true false config.py azure-cosmos-python-3.1.1/samples/ChangeFeedManagement/Program.py000066400000000000000000000120561352206500100250530ustar00rootroot00000000000000import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.errors as errors import datetime import samples.Shared.config as cfg # ---------------------------------------------------------------------------------------------------------- # Prerequistes - # # 1. An Azure Cosmos account - # https:#azure.microsoft.com/en-us/documentation/articles/documentdb-create-account/ # # 2. Microsoft Azure Cosmos PyPi package - # https://pypi.python.org/pypi/azure-cosmos/ # ---------------------------------------------------------------------------------------------------------- # Sample - demonstrates how to consume the Change Feed and iterate on the results. # ---------------------------------------------------------------------------------------------------------- HOST = cfg.settings['host'] MASTER_KEY = cfg.settings['master_key'] DATABASE_ID = cfg.settings['database_id'] COLLECTION_ID = cfg.settings['collection_id'] database_link = 'dbs/' + DATABASE_ID collection_link = database_link + '/colls/' + COLLECTION_ID class IDisposable(cosmos_client.CosmosClient): """ A context manager to automatically close an object with a close method in a with statement. """ def __init__(self, obj): self.obj = obj def __enter__(self): return self.obj # bound to target def __exit__(self, exception_type, exception_val, trace): # extra cleanup in here self.obj = None class ChangeFeedManagement: @staticmethod def CreateDocuments(client): print('Creating Documents') for i in range(1, 1000): c = str(i) document_definition = {'id': 'document'+ c, 'address': {'street': '1 Microsoft Way'+c, 'city': 'Redmond'+c, 'state': 'WA', 'zip code': 98052 } } created_document = client.CreateItem( collection_link, document_definition) @staticmethod def ReadFeed(client): print('\nReading Change Feed from the beginning\n') options = {} # For a particular Partition Key Range we can use options['partitionKeyRangeId'] options["startFromBeginning"] = True # Start from beginning will read from the beginning of the history of the collection # If no startFromBeginning is specified, the read change feed loop will pickup the documents that happen while the loop / process is active response = client.QueryItemsChangeFeed(collection_link, options) for doc in response: print(doc) print('\nFinished reading all the change feed\n') @staticmethod def ReadFeedForTime(client, time): print('\nReading Change Feed from point in time\n') options = {} # Define a point in time to start reading the feed from options["startTime"] = time response = client.QueryItemsChangeFeed(collection_link, options) for doc in response: print(doc) print('\nFinished reading all the changes from point in time\n') def run_sample(): with IDisposable(cosmos_client.CosmosClient(HOST, {'masterKey': MASTER_KEY} )) as client: try: # setup database for this sample try: client.CreateDatabase({"id": DATABASE_ID}) except errors.HTTPFailure as e: if e.status_code == 409: pass else: raise # setup collection for this sample collection_definition = { 'id': COLLECTION_ID, 'partitionKey': { 'paths': ['/address/state'], 'kind': documents.PartitionKind.Hash } } try: client.CreateContainer(database_link, collection_definition) print('Collection with id \'{0}\' created'.format(COLLECTION_ID)) except errors.HTTPFailure as e: if e.status_code == 409: print('Collection with id \'{0}\' was found'.format(COLLECTION_ID)) else: raise errors.HTTPFailure(e.status_code) ChangeFeedManagement.CreateDocuments(client) ChangeFeedManagement.ReadFeed(client) time = datetime.datetime.now() ChangeFeedManagement.CreateDocuments(client) ChangeFeedManagement.ReadFeedForTime(client, time) except errors.HTTPFailure as e: print('\nrun_sample has caught an error. {0}'.format(e)) finally: print("\nrun_sample done") if __name__ == '__main__': try: run_sample() except Exception as e: print("Top level Error: args:{0}, message:N/A".format(e.args)) azure-cosmos-python-3.1.1/samples/CollectionManagement/000077500000000000000000000000001352206500100232105ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/CollectionManagement/CollectionManagement.pyproj000066400000000000000000000033761352206500100305560ustar00rootroot00000000000000 Debug 2.0 d65bad79-205d-4b95-bc98-294974eab39c . Program.py ..\Shared\;..\..\ . . CollectionManagement CollectionManagement true false true false config.py 10.0 azure-cosmos-python-3.1.1/samples/CollectionManagement/Program.py000066400000000000000000000312241352206500100251730ustar00rootroot00000000000000import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.errors as errors import samples.Shared.config as cfg # ---------------------------------------------------------------------------------------------------------- # Prerequistes - # # 1. An Azure Cosmos account - # https://azure.microsoft.com/en-us/documentation/articles/documentdb-create-account/ # # 2. Microsoft Azure Cosmos PyPi package - # https://pypi.python.org/pypi/azure-cosmos/ # ---------------------------------------------------------------------------------------------------------- # Sample - demonstrates the basic CRUD operations on a Collection resource for Azure Cosmos # # 1. Query for Collection # # 2. Create Collection # 2.1 - Basic Create # 2.2 - Create collection with custom IndexPolicy # 2.3 - Create collection with offer throughput set # 2.4 - Create collection with unique key # 2.5 - Create Collection with partition key # 2.6 - Create Collection with partition key V2 # # 3. Manage Collection Offer Throughput # 3.1 - Get Collection performance tier # 3.2 - Change performance tier # # 4. Get a Collection by its Id property # # 5. List all Collection resources in a Database # # 6. Delete Collection # ---------------------------------------------------------------------------------------------------------- # Note - # # Running this sample will create (and delete) multiple DocumentContainers on your account. # Each time a DocumentContainer is created the account will be billed for 1 hour of usage based on # the performance tier of that account. # ---------------------------------------------------------------------------------------------------------- HOST = cfg.settings['host'] MASTER_KEY = cfg.settings['master_key'] DATABASE_ID = cfg.settings['database_id'] COLLECTION_ID = cfg.settings['collection_id'] database_link = 'dbs/' + DATABASE_ID class IDisposable(cosmos_client.CosmosClient): """ A context manager to automatically close an object with a close method in a with statement. """ def __init__(self, obj): self.obj = obj def __enter__(self): return self.obj # bound to target def __exit__(self, exception_type, exception_val, trace): # extra cleanup in here self.obj = None class CollectionManagement: @staticmethod def find_Container(client, id): print('1. Query for Collection') collections = list(client.QueryContainers( database_link, { "query": "SELECT * FROM r WHERE r.id=@id", "parameters": [ { "name":"@id", "value": id } ] } )) if len(collections) > 0: print('Collection with id \'{0}\' was found'.format(id)) else: print('No collection with id \'{0}\' was found'. format(id)) @staticmethod def create_Container(client, id): """ Execute the most basic Create of collection. This will create a collection with 400 RUs throughput and default indexing policy """ print("\n2.1 Create Collection - Basic") try: client.CreateContainer(database_link, {"id": id}) print('Collection with id \'{0}\' created'.format(id)) except errors.HTTPFailure as e: if e.status_code == 409: print('A collection with id \'{0}\' already exists'.format(id)) else: raise print("\n2.2 Create Collection - With custom index policy") try: coll = { "id": "collection_custom_index_policy", "indexingPolicy": { "indexingMode": "lazy", "automatic": False } } collection = client.CreateContainer(database_link, coll) print('Collection with id \'{0}\' created'.format(collection['id'])) print('IndexPolicy Mode - \'{0}\''.format(collection['indexingPolicy']['indexingMode'])) print('IndexPolicy Automatic - \'{0}\''.format(collection['indexingPolicy']['automatic'])) except errors.CosmosError as e: if e.status_code == 409: print('A collection with id \'{0}\' already exists'.format(collection['id'])) else: raise print("\n2.3 Create Collection - With custom offer throughput") try: coll = {"id": "collection_custom_throughput"} collection_options = { 'offerThroughput': 400 } collection = client.CreateContainer(database_link, coll, collection_options ) print('Collection with id \'{0}\' created'.format(collection['id'])) except errors.HTTPFailure as e: if e.status_code == 409: print('A collection with id \'{0}\' already exists'.format(collection['id'])) else: raise print("\n2.4 Create Collection - With Unique keys") try: coll = {"id": "collection_unique_keys", 'uniqueKeyPolicy': {'uniqueKeys': [{'paths': ['/field1/field2', '/field3']}]}} collection_options = { 'offerThroughput': 400 } collection = client.CreateContainer(database_link, coll, collection_options ) unique_key_paths = collection['uniqueKeyPolicy']['uniqueKeys'][0]['paths'] print('Collection with id \'{0}\' created'.format(collection['id'])) print('Unique Key Paths - \'{0}\', \'{1}\''.format(unique_key_paths[0], unique_key_paths[1])) except errors.HTTPFailure as e: if e.status_code == 409: print('A collection with id \'{0}\' already exists'.format(collection['id'])) else: raise print("\n2.5 Create Collection - With Partition key") try: coll = { "id": "collection_partition_key", "partitionKey": { "paths": [ "/field1" ], "kind": "Hash" } } collection = client.CreateContainer(database_link, coll) print('Collection with id \'{0}\' created'.format(collection['id'])) print('Partition Key - \'{0}\''.format(collection['partitionKey'])) except errors.CosmosError as e: if e.status_code == 409: print('A collection with id \'{0}\' already exists'.format(collection['id'])) else: raise print("\n2.6 Create Collection - With Partition key V2") try: coll = { "id": "collection_partition_key_v2", "partitionKey": { "paths": [ "/field1" ], "kind": "Hash", "version": 2 } } collection = client.CreateContainer(database_link, coll) print('Collection with id \'{0}\' created'.format(collection['id'])) print('Partition Key - \'{0}\''.format(collection['partitionKey'])) except errors.CosmosError as e: if e.status_code == 409: print('A collection with id \'{0}\' already exists'.format(collection['id'])) else: raise @staticmethod def manage_offer_throughput(client, id): print("\n3.1 Get Collection Performance tier") #A Collection's Offer Throughput determines the performance throughput of a collection. #A Collection is loosely coupled to Offer through the Offer's offerResourceId #Offer.offerResourceId == Collection._rid #Offer.resource == Collection._self try: # read the collection, so we can get its _self collection_link = database_link + '/colls/{0}'.format(id) collection = client.ReadContainer(collection_link) # now use its _self to query for Offers offer = list(client.QueryOffers('SELECT * FROM c WHERE c.resource = \'{0}\''.format(collection['_self'])))[0] print('Found Offer \'{0}\' for Collection \'{1}\' and its throughput is \'{2}\''.format(offer['id'], collection['_self'], offer['content']['offerThroughput'])) except errors.HTTPFailure as e: if e.status_code == 404: print('A collection with id \'{0}\' does not exist'.format(id)) else: raise print("\n3.2 Change Offer Throughput of Collection") #The Offer Throughput of a collection controls the throughput allocated to the Collection #To increase (or decrease) the throughput of any Collection you need to adjust the Offer.content.offerThroughput #of the Offer record linked to the Collection #The following code shows how you can change Collection's throughput offer['content']['offerThroughput'] += 100 offer = client.ReplaceOffer(offer['_self'], offer) print('Replaced Offer. Offer Throughput is now \'{0}\''.format(offer['content']['offerThroughput'])) @staticmethod def read_Container(client, id): print("\n4. Get a Collection by id") try: # All Azure Cosmos resources are addressable via a link # This link is constructed from a combination of resource hierachy and # the resource id. # Eg. The link for collection with an id of Bar in database Foo would be dbs/Foo/colls/Bar collection_link = database_link + '/colls/{0}'.format(id) collection = client.ReadContainer(collection_link) print('Collection with id \'{0}\' was found, it\'s _self is {1}'.format(collection['id'], collection['_self'])) except errors.HTTPFailure as e: if e.status_code == 404: print('A collection with id \'{0}\' does not exist'.format(id)) else: raise @staticmethod def list_Containers(client): print("\n5. List all Collection in a Database") print('Collections:') collections = list(client.ReadContainers(database_link)) if not collections: return for collection in collections: print(collection['id']) @staticmethod def delete_Container(client, id): print("\n6. Delete Collection") try: collection_link = database_link + '/colls/{0}'.format(id) client.DeleteContainer(collection_link) print('Collection with id \'{0}\' was deleted'.format(id)) except errors.HTTPFailure as e: if e.status_code == 404: print('A collection with id \'{0}\' does not exist'.format(id)) else: raise def run_sample(): with IDisposable(cosmos_client.CosmosClient(HOST, {'masterKey': MASTER_KEY} )) as client: try: # setup database for this sample try: client.CreateDatabase({"id": DATABASE_ID}) except errors.HTTPFailure as e: if e.status_code == 409: pass else: raise # query for a collection CollectionManagement.find_Container(client, COLLECTION_ID) # create a collection CollectionManagement.create_Container(client, COLLECTION_ID) # get & change Offer Throughput of collection CollectionManagement.manage_offer_throughput(client, COLLECTION_ID) # get a collection using its id CollectionManagement.read_Container(client, COLLECTION_ID) # list all collection on an account CollectionManagement.list_Containers(client) # delete collection by id CollectionManagement.delete_Container(client, COLLECTION_ID) # cleanup database after sample try: client.DeleteDatabase(database_link) except errors.CosmosError as e: if e.status_code == 404: pass else: raise errors.HTTPFailure(e.status_code) except errors.HTTPFailure as e: print('\nrun_sample has caught an error. {0}'.format(e)) finally: print("\nrun_sample done") if __name__ == '__main__': try: run_sample() except Exception as e: print("Top level Error: args:{0}, message:{1}".format(e.args,e)) azure-cosmos-python-3.1.1/samples/DatabaseManagement/000077500000000000000000000000001352206500100226215ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/DatabaseManagement/DatabaseManagement.pyproj000066400000000000000000000036701352206500100275750ustar00rootroot00000000000000 Debug 2.0 9e81a024-996c-4c59-a9ef-e4e78afbb3bf . Program.py ..\Shared\;..\..\ . . DatabaseManagement DatabaseManagement Global|PythonCore|3.6 true false true false config.py Code 10.0 azure-cosmos-python-3.1.1/samples/DatabaseManagement/Program.py000066400000000000000000000117671352206500100246160ustar00rootroot00000000000000import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.errors as errors import samples.Shared.config as cfg # ---------------------------------------------------------------------------------------------------------- # Prerequistes - # # 1. An Azure Cosmos account - # https://docs.microsoft.com/azure/cosmos-db/create-sql-api-python#create-a-database-account # # 2. Microsoft Azure Cosmos PyPi package - # https://pypi.python.org/pypi/azure-cosmos/ # ---------------------------------------------------------------------------------------------------------- # Sample - demonstrates the basic CRUD operations on a Database resource for Azure Cosmos # # 1. Query for Database (QueryDatabases) # # 2. Create Database (CreateDatabase) # # 3. Get a Database by its Id property (ReadDatabase) # # 4. List all Database resources on an account (ReadDatabases) # # 5. Delete a Database given its Id property (DeleteDatabase) # ---------------------------------------------------------------------------------------------------------- HOST = cfg.settings['host'] MASTER_KEY = cfg.settings['master_key'] DATABASE_ID = cfg.settings['database_id'] class IDisposable: """ A context manager to automatically close an object with a close method in a with statement. """ def __init__(self, obj): self.obj = obj def __enter__(self): return self.obj # bound to target def __exit__(self, exception_type, exception_val, trace): # extra cleanup in here self.obj = None class DatabaseManagement: @staticmethod def find_database(client, id): print('1. Query for Database') databases = list(client.QueryDatabases({ "query": "SELECT * FROM r WHERE r.id=@id", "parameters": [ { "name":"@id", "value": id } ] })) if len(databases) > 0: print('Database with id \'{0}\' was found'.format(id)) else: print('No database with id \'{0}\' was found'. format(id)) @staticmethod def create_database(client, id): print("\n2. Create Database") try: client.CreateDatabase({"id": id}) print('Database with id \'{0}\' created'.format(id)) except errors.HTTPFailure as e: if e.status_code == 409: print('A database with id \'{0}\' already exists'.format(id)) else: raise @staticmethod def read_database(client, id): print("\n3. Get a Database by id") try: # All Azure Cosmos resources are addressable via a link # This link is constructed from a combination of resource hierachy and # the resource id. # Eg. The link for database with an id of Foo would be dbs/Foo database_link = 'dbs/' + id database = client.ReadDatabase(database_link) print('Database with id \'{0}\' was found, it\'s _self is {1}'.format(id, database['_self'])) except errors.HTTPFailure as e: if e.status_code == 404: print('A database with id \'{0}\' does not exist'.format(id)) else: raise @staticmethod def list_databases(client): print("\n4. List all Databases on an account") print('Databases:') databases = list(client.ReadDatabases()) if not databases: return for database in databases: print(database['id']) @staticmethod def delete_database(client, id): print("\n5. Delete Database") try: database_link = 'dbs/' + id client.DeleteDatabase(database_link) print('Database with id \'{0}\' was deleted'.format(id)) except errors.HTTPFailure as e: if e.status_code == 404: print('A database with id \'{0}\' does not exist'.format(id)) else: raise def run_sample(): with IDisposable(cosmos_client.CosmosClient(HOST, {'masterKey': MASTER_KEY} )) as client: try: # query for a database DatabaseManagement.find_database(client, DATABASE_ID) # create a database DatabaseManagement.create_database(client, DATABASE_ID) # get a database using its id DatabaseManagement.read_database(client, DATABASE_ID) # list all databases on an account DatabaseManagement.list_databases(client) # delete database by id DatabaseManagement.delete_database(client, DATABASE_ID) except errors.HTTPFailure as e: print('\nrun_sample has caught an error. {0}'.format(e)) finally: print("\nrun_sample done") if __name__ == '__main__': try: run_sample() except Exception as e: print("Top level Error: args:{0}, message:{1}".format(e.args,e))azure-cosmos-python-3.1.1/samples/DocumentDB.Samples.sln000066400000000000000000000050671352206500100232350ustar00rootroot00000000000000 Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27004.2008 MinimumVisualStudioVersion = 10.0.40219.1 Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "DatabaseManagement", "DatabaseManagement\DatabaseManagement.pyproj", "{9E81A024-996C-4C59-A9EF-E4E78AFBB3BF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{D86C5C17-FA0D-41E0-9425-5B2110E53BD9}" ProjectSection(SolutionItems) = preProject Shared\config.py = Shared\config.py Shared\PossibleOptions.txt = Shared\PossibleOptions.txt EndProjectSection EndProject Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "CollectionManagement", "CollectionManagement\CollectionManagement.pyproj", "{D65BAD79-205D-4B95-BC98-294974EAB39C}" EndProject Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "DocumentManagement", "DocumentManagement\DocumentManagement.pyproj", "{976E1CAA-D0A9-40AA-B4C2-1A0B4DB71703}" EndProject Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "IndexManagement", "IndexManagement\IndexManagement.pyproj", "{B1869509-4DC8-4AA8-8F2A-220FDBC14154}" EndProject Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "MultiMasterOperations", "MultiMasterOperations\MultiMasterOperations.pyproj", "{F29D7A3E-CB9A-4B15-BDA1-BB30F3CA9CFA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9E81A024-996C-4C59-A9EF-E4E78AFBB3BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9E81A024-996C-4C59-A9EF-E4E78AFBB3BF}.Release|Any CPU.ActiveCfg = Release|Any CPU {D65BAD79-205D-4B95-BC98-294974EAB39C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D65BAD79-205D-4B95-BC98-294974EAB39C}.Release|Any CPU.ActiveCfg = Release|Any CPU {976E1CAA-D0A9-40AA-B4C2-1A0B4DB71703}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {976E1CAA-D0A9-40AA-B4C2-1A0B4DB71703}.Release|Any CPU.ActiveCfg = Release|Any CPU {B1869509-4DC8-4AA8-8F2A-220FDBC14154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B1869509-4DC8-4AA8-8F2A-220FDBC14154}.Release|Any CPU.ActiveCfg = Release|Any CPU {F29D7A3E-CB9A-4B15-BDA1-BB30F3CA9CFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F29D7A3E-CB9A-4B15-BDA1-BB30F3CA9CFA}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {234DBA1B-B556-466F-BD7C-2B0230AC2DD9} EndGlobalSection EndGlobal azure-cosmos-python-3.1.1/samples/DocumentManagement/000077500000000000000000000000001352206500100226735ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/DocumentManagement/DocumentManagement.pyproj000066400000000000000000000031041352206500100277110ustar00rootroot00000000000000 Debug 2.0 976e1caa-d0a9-40aa-b4c2-1a0b4db71703 . Program.py ..\Shared . . DocumentManagement DocumentManagement true false true false config.py azure-cosmos-python-3.1.1/samples/DocumentManagement/Program.py000066400000000000000000000162321352206500100246600ustar00rootroot00000000000000import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.errors as errors import datetime import samples.Shared.config as cfg # ---------------------------------------------------------------------------------------------------------- # Prerequistes - # # 1. An Azure Cosmos account - # https:#azure.microsoft.com/en-us/documentation/articles/documentdb-create-account/ # # 2. Microsoft Azure Cosmos PyPi package - # https://pypi.python.org/pypi/azure-cosmos/ # ---------------------------------------------------------------------------------------------------------- # Sample - demonstrates the basic CRUD operations on a Database resource for Azure Cosmos # # 1. Query for Database (QueryDatabases) # # 2. Create Database (CreateDatabase) # # 3. Get a Database by its Id property (ReadDatabase) # # 4. List all Database resources on an account (ReadDatabases) # # 5. Delete a Database given its Id property (DeleteDatabase) # ---------------------------------------------------------------------------------------------------------- HOST = cfg.settings['host'] MASTER_KEY = cfg.settings['master_key'] DATABASE_ID = cfg.settings['database_id'] COLLECTION_ID = cfg.settings['collection_id'] database_link = 'dbs/' + DATABASE_ID collection_link = database_link + '/colls/' + COLLECTION_ID class IDisposable(cosmos_client.CosmosClient): """ A context manager to automatically close an object with a close method in a with statement. """ def __init__(self, obj): self.obj = obj def __enter__(self): return self.obj # bound to target def __exit__(self, exception_type, exception_val, trace): # extra cleanup in here self.obj = None class DocumentManagement: @staticmethod def CreateDocuments(client): print('Creating Documents') # Create a SalesOrder object. This object has nested properties and various types including numbers, DateTimes and strings. # This can be saved as JSON as is without converting into rows/columns. sales_order = DocumentManagement.GetSalesOrder("SalesOrder1") client.CreateItem(collection_link, sales_order) # As your app evolves, let's say your object has a new schema. You can insert SalesOrderV2 objects without any # changes to the database tier. sales_order2 = DocumentManagement.GetSalesOrderV2("SalesOrder2") client.CreateItem(collection_link, sales_order2) @staticmethod def ReadDocument(client, doc_id): print('\n1.2 Reading Document by Id\n') # Note that Reads require a partition key to be spcified. This can be skipped if your collection is not # partitioned i.e. does not have a partition key definition during creation. doc_link = collection_link + '/docs/' + doc_id response = client.ReadItem(doc_link) print('Document read by Id {0}'.format(doc_id)) print('Account Number: {0}'.format(response.get('account_number'))) @staticmethod def ReadDocuments(client): print('\n1.3 - Reading all documents in a collection\n') # NOTE: Use MaxItemCount on Options to control how many documents come back per trip to the server # Important to handle throttles whenever you are doing operations such as this that might # result in a 429 (throttled request) documentlist = list(client.ReadItems(collection_link, {'maxItemCount':10})) print('Found {0} documents'.format(documentlist.__len__())) for doc in documentlist: print('Document Id: {0}'.format(doc.get('id'))) @staticmethod def GetSalesOrder(document_id): order1 = {'id' : document_id, 'account_number' : 'Account1', 'purchase_order_number' : 'PO18009186470', 'order_date' : datetime.date(2005,1,10).strftime('%c'), 'subtotal' : 419.4589, 'tax_amount' : 12.5838, 'freight' : 472.3108, 'total_due' : 985.018, 'items' : [ {'order_qty' : 1, 'product_id' : 100, 'unit_price' : 418.4589, 'line_price' : 418.4589 } ], 'ttl' : 60 * 60 * 24 * 30 } return order1 @staticmethod def GetSalesOrderV2(document_id): # notice new fields have been added to the sales order order2 = {'id' : document_id, 'account_number' : 'Account2', 'purchase_order_number' : 'PO15428132599', 'order_date' : datetime.date(2005,7,11).strftime('%c'), 'due_date' : datetime.date(2005,7,21).strftime('%c'), 'shipped_date' : datetime.date(2005,7,15).strftime('%c'), 'subtotal' : 6107.0820, 'tax_amount' : 586.1203, 'freight' : 183.1626, 'discount_amt' : 1982.872, 'total_due' : 4893.3929, 'items' : [ {'order_qty' : 3, 'product_code' : 'A-123', # notice how in item details we no longer reference a ProductId 'product_name' : 'Product 1', # instead we have decided to denormalise our schema and include 'currency_symbol' : '$', # the Product details relevant to the Order on to the Order directly 'currecny_code' : 'USD', # this is a typical refactor that happens in the course of an application 'unit_price' : 17.1, # that would have previously required schema changes and data migrations etc. 'line_price' : 5.7 } ], 'ttl' : 60 * 60 * 24 * 30 } return order2 def run_sample(): with IDisposable(cosmos_client.CosmosClient(HOST, {'masterKey': MASTER_KEY} )) as client: try: # setup database for this sample try: client.CreateDatabase({"id": DATABASE_ID}) except errors.HTTPFailure as e: if e.status_code == 409: pass else: raise # setup collection for this sample try: client.CreateContainer(database_link, {"id": COLLECTION_ID}) print('Collection with id \'{0}\' created'.format(COLLECTION_ID)) except errors.HTTPFailure as e: if e.status_code == 409: print('Collection with id \'{0}\' was found'.format(COLLECTION_ID)) else: raise DocumentManagement.CreateDocuments(client) DocumentManagement.ReadDocument(client,'SalesOrder1') DocumentManagement.ReadDocuments(client) except errors.HTTPFailure as e: print('\nrun_sample has caught an error. {0}'.format(e)) finally: print("\nrun_sample done") if __name__ == '__main__': try: run_sample() except Exception as e: print("Top level Error: args:{0}, message:N/A".format(e.args)) azure-cosmos-python-3.1.1/samples/IndexManagement/000077500000000000000000000000001352206500100221645ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/IndexManagement/IndexManagement.pyproj000066400000000000000000000031021352206500100264710ustar00rootroot00000000000000 Debug 2.0 b1869509-4dc8-4aa8-8f2a-220fdbc14154 . IndexManagement.py . . IndexManagement IndexManagement true false true false config.py azure-cosmos-python-3.1.1/samples/IndexManagement/Program.py000066400000000000000000000767001352206500100241570ustar00rootroot00000000000000import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.errors as errors import requests import traceback import urllib3 from requests.utils import DEFAULT_CA_BUNDLE_PATH as CaCertPath import samples.Shared.config as cfg HOST = cfg.settings['host'] MASTER_KEY = cfg.settings['master_key'] DATABASE_ID = cfg.settings['database_id'] COLLECTION_ID = "index-samples" # A typical collection has the following properties within it's indexingPolicy property # indexingMode # automatic # includedPaths # excludedPaths # # We can toggle 'automatic' to eiher be True or False depending upon whether we want to have indexing over all columns by default or not. # indexingMode can be either of consistent, lazy or none # # We can provide options while creating documents. indexingDirective is one such, # by which we can tell whether it should be included or excluded in the index of the parent collection. # indexingDirective can be either 'Include', 'Exclude' or 'Default' # To run this Demo, please provide your own CA certs file or download one from # http://curl.haxx.se/docs/caextract.html # Setup the certificate file in .pem format. # If you still get an SSLError, try disabling certificate verification and suppress warnings def ObtainClient(): connection_policy = documents.ConnectionPolicy() connection_policy.SSLConfiguration = documents.SSLConfiguration() # Try to setup the cacert.pem # connection_policy.SSLConfiguration.SSLCaCerts = CaCertPath # Else, disable verification urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) connection_policy.SSLConfiguration.SSLCaCerts = False return cosmos_client.CosmosClient(HOST, {'masterKey': MASTER_KEY}, connection_policy) def test_ssl_connection(): client = ObtainClient() # Read databases after creation. try: databases = list(client.ReadDatabases()) print(databases) return True except requests.exceptions.SSLError as e: print("SSL error occured. ", e) except OSError as e: print("OSError occured. ", e) except Exception as e: print(traceback.format_exc()) return False def GetDatabaseLink(database_id): return "dbs" + "/" + database_id def GetContainerLink(database_id, collection_id): return GetDatabaseLink(database_id) + "/" + "colls" + "/" + collection_id def GetDocumentLink(database_id, collection_id, document_id): return GetContainerLink(database_id, collection_id) + "/" + "docs" + "/" + document_id # Query for Entity / Entities def Query_Entities(client, entity_type, id = None, parent_link = None): find_entity_by_id_query = { "query": "SELECT * FROM r WHERE r.id=@id", "parameters": [ { "name":"@id", "value": id } ] } entities = None try: if entity_type == 'database': if id == None: entities = list(client.ReadDatabases()) else: entities = list(client.QueryDatabases(find_entity_by_id_query)) elif entity_type == 'collection': if parent_link == None: raise ValueError('Database link not provided to search collection(s)') if id == None: entities = list(client.ReadContainers(parent_link)) else: entities = list(client.QueryContainers(parent_link, find_entity_by_id_query)) elif entity_type == 'document': if parent_link == None: raise ValueError('Database / Collection link not provided to search document(s)') if id == None: entities = list(client.ReadItems(parent_link)) else: entities = list(client.QueryItems(parent_link, find_entity_by_id_query)) except errors.CosmosError as e: print("The following error occured while querying for the entity / entities ", entity_type, id if id != None else "") print(e) raise if id == None: return entities if len(entities) == 1: return entities[0] return None def CreateDatabaseIfNotExists(client, database_id): try: database = Query_Entities(client, 'database', id = database_id) if database == None: database = client.CreateDatabase({"id": database_id}) return database except errors.HTTPFailure as e: if e.status_code == 409: # Move these constants to an enum pass else: raise errors.HTTPFailure(e.status_code) def DeleteContainerIfExists(client, database_id, collection_id): try: collection_link = GetContainerLink(database_id, collection_id) client.DeleteContainer(collection_link) print('Collection with id \'{0}\' was deleted'.format(collection_id)) except errors.HTTPFailure as e: if e.status_code == 404: pass elif e.status_code == 400: print("Bad request for collection link", collection_link) raise else: raise def print_dictionary_items(dict): for k, v in dict.items(): print("{:<15}".format(k), v) print() def FetchAllDatabases(client): databases = Query_Entities(client, 'database') print("-" * 41) print("-" * 41) for db in databases: print_dictionary_items(db) print("-" * 41) def QueryDocumentsWithCustomQuery(client, collection_link, query_with_optional_parameters, message = "Document(s) found by query: "): try: results = list(client.QueryItems(collection_link, query_with_optional_parameters)) print(message) for doc in results: print(doc) return results except errors.HTTPFailure as e: if e.status_code == 404: print("Document doesn't exist") elif e.status_code == 400: # Can occur when we are trying to query on excluded paths print("Bad Request exception occured: ", e) pass else: raise finally: print() def ExplicitlyExcludeFromIndex(client, database_id): """ The default index policy on a DocumentContainer will AUTOMATICALLY index ALL documents added. There may be scenarios where you want to exclude a specific doc from the index even though all other documents are being indexed automatically. This method demonstrates how to use an index directive to control this """ try: DeleteContainerIfExists(client, database_id, COLLECTION_ID) database_link = GetDatabaseLink(database_id) # collections = Query_Entities(client, 'collection', parent_link = database_link) # print(collections) # Create a collection with default index policy (i.e. automatic = true) created_Container = client.CreateContainer(database_link, {"id" : COLLECTION_ID}) print(created_Container) print("\n" + "-" * 25 + "\n1. Collection created with index policy") print_dictionary_items(created_Container["indexingPolicy"]) # Create a document and query on it immediately. # Will work as automatic indexing is still True collection_link = GetContainerLink(database_id, COLLECTION_ID) doc = client.CreateItem(collection_link, { "id" : "doc1", "orderId" : "order1" }) print("\n" + "-" * 25 + "Document doc1 created with order1" + "-" * 25) print(doc) query = { "query": "SELECT * FROM r WHERE r.orderId=@orderNo", "parameters": [ { "name":"@orderNo", "value": "order1" } ] } QueryDocumentsWithCustomQuery(client, collection_link, query) # Now, create a document but this time explictly exclude it from the collection using IndexingDirective # Then query for that document # Shoud NOT find it, because we excluded it from the index # BUT, the document is there and doing a ReadDocument by Id will prove it doc2 = client.CreateItem(collection_link, { "id" : "doc2", "orderId" : "order2" }, {'indexingDirective' : documents.IndexingDirective.Exclude}) print("\n" + "-" * 25 + "Document doc2 created with order2" + "-" * 25) print(doc2) query = { "query": "SELECT * FROM r WHERE r.orderId=@orderNo", "parameters": [ { "name":"@orderNo", "value": "order2" } ] } QueryDocumentsWithCustomQuery(client, collection_link, query) docRead = client.ReadItem(GetDocumentLink(database_id, COLLECTION_ID, "doc2")) print("Document read by ID: \n", docRead["id"]) # Cleanup client.DeleteContainer(collection_link) print("\n") except errors.HTTPFailure as e: if e.status_code == 409: print("Entity already exists") elif e.status_code == 404: print("Entity doesn't exist") else: raise def UseManualIndexing(client, database_id): """The default index policy on a DocumentContainer will AUTOMATICALLY index ALL documents added. There may be cases where you can want to turn-off automatic indexing and only selectively add only specific documents to the index. This method demonstrates how to control this by setting the value of automatic within indexingPolicy to False """ try: DeleteContainerIfExists(client, database_id, COLLECTION_ID) database_link = GetDatabaseLink(database_id) # collections = Query_Entities(client, 'collection', parent_link = database_link) # print(collections) # Create a collection with manual (instead of automatic) indexing created_Container = client.CreateContainer(database_link, {"id" : COLLECTION_ID, "indexingPolicy" : { "automatic" : False} }) print(created_Container) print("\n" + "-" * 25 + "\n2. Collection created with index policy") print_dictionary_items(created_Container["indexingPolicy"]) # Create a document # Then query for that document # We should find nothing, because automatic indexing on the collection level is False # BUT, the document is there and doing a ReadDocument by Id will prove it collection_link = GetContainerLink(database_id, COLLECTION_ID) doc = client.CreateItem(collection_link, { "id" : "doc1", "orderId" : "order1" }) print("\n" + "-" * 25 + "Document doc1 created with order1" + "-" * 25) print(doc) query = { "query": "SELECT * FROM r WHERE r.orderId=@orderNo", "parameters": [ { "name":"@orderNo", "value": "order1" } ] } QueryDocumentsWithCustomQuery(client, collection_link, query) docRead = client.ReadItem(GetDocumentLink(database_id, COLLECTION_ID, "doc1")) print("Document read by ID: \n", docRead["id"]) # Now create a document, passing in an IndexingDirective saying we want to specifically index this document # Query for the document again and this time we should find it because we manually included the document in the index doc2 = client.CreateItem(collection_link, { "id" : "doc2", "orderId" : "order2" }, {'indexingDirective' : documents.IndexingDirective.Include}) print("\n" + "-" * 25 + "Document doc2 created with order2" + "-" * 25) print(doc2) query = { "query": "SELECT * FROM r WHERE r.orderId=@orderNo", "parameters": [ { "name":"@orderNo", "value": "order2" } ] } QueryDocumentsWithCustomQuery(client, collection_link, query) # Cleanup client.DeleteContainer(collection_link) print("\n") except errors.HTTPFailure as e: if e.status_code == 409: print("Entity already exists") elif e.status_code == 404: print("Entity doesn't exist") else: raise def ExcludePathsFromIndex(client, database_id): """The default behavior is for Cosmos to index every attribute in every document automatically. There are times when a document contains large amounts of information, in deeply nested structures that you know you will never search on. In extreme cases like this, you can exclude paths from the index to save on storage cost, improve write performance and also improve read performance because the index is smaller This method demonstrates how to set excludedPaths within indexingPolicy """ try: DeleteContainerIfExists(client, database_id, COLLECTION_ID) database_link = GetDatabaseLink(database_id) # collections = Query_Entities(client, 'collection', parent_link = database_link) # print(collections) doc_with_nested_structures = { "id" : "doc1", "foo" : "bar", "metaData" : "meta", "subDoc" : { "searchable" : "searchable", "nonSearchable" : "value" }, "excludedNode" : { "subExcluded" : "something", "subExcludedNode" : { "someProperty" : "value" } } } collection_to_create = { "id" : COLLECTION_ID , "indexingPolicy" : { "includedPaths" : [ {'path' : "/*"} ], # Special mandatory path of "/*" required to denote include entire tree "excludedPaths" : [ {'path' : "/metaData/*"}, # exclude metaData node, and anything under it {'path' : "/subDoc/nonSearchable/*"}, # exclude ONLY a part of subDoc {'path' : "/\"excludedNode\"/*"} # exclude excludedNode node, and anything under it ] } } print(collection_to_create) print(doc_with_nested_structures) # Create a collection with the defined properties # The effect of the above IndexingPolicy is that only id, foo, and the subDoc/searchable are indexed created_Container = client.CreateContainer(database_link, collection_to_create) print(created_Container) print("\n" + "-" * 25 + "\n4. Collection created with index policy") print_dictionary_items(created_Container["indexingPolicy"]) # The effect of the above IndexingPolicy is that only id, foo, and the subDoc/searchable are indexed collection_link = GetContainerLink(database_id, COLLECTION_ID) doc = client.CreateItem(collection_link, doc_with_nested_structures) print("\n" + "-" * 25 + "Document doc1 created with nested structures" + "-" * 25) print(doc) # Querying for a document on either metaData or /subDoc/subSubDoc/someProperty > fail because these paths were excluded and they raise a BadRequest(400) Exception query = {"query": "SELECT * FROM r WHERE r.metaData=@desiredValue", "parameters" : [{ "name":"@desiredValue", "value": "meta" }]} QueryDocumentsWithCustomQuery(client, collection_link, query) query = {"query": "SELECT * FROM r WHERE r.subDoc.nonSearchable=@desiredValue", "parameters" : [{ "name":"@desiredValue", "value": "value" }]} QueryDocumentsWithCustomQuery(client, collection_link, query) query = {"query": "SELECT * FROM r WHERE r.excludedNode.subExcludedNode.someProperty=@desiredValue", "parameters" : [{ "name":"@desiredValue", "value": "value" }]} QueryDocumentsWithCustomQuery(client, collection_link, query) # Querying for a document using foo, or even subDoc/searchable > succeed because they were not excluded query = {"query": "SELECT * FROM r WHERE r.foo=@desiredValue", "parameters" : [{ "name":"@desiredValue", "value": "bar" }]} QueryDocumentsWithCustomQuery(client, collection_link, query) query = {"query": "SELECT * FROM r WHERE r.subDoc.searchable=@desiredValue", "parameters" : [{ "name":"@desiredValue", "value": "searchable" }]} QueryDocumentsWithCustomQuery(client, collection_link, query) # Cleanup client.DeleteContainer(collection_link) print("\n") except errors.HTTPFailure as e: if e.status_code == 409: print("Entity already exists") elif e.status_code == 404: print("Entity doesn't exist") else: raise def RangeScanOnHashIndex(client, database_id): """When a range index is not available (i.e. Only hash or no index found on the path), comparisons queries can still be performed as scans using Allow scan request headers passed through options This method demonstrates how to force a scan when only hash indexes exist on the path ===== Warning===== This was made an opt-in model by design. Scanning is an expensive operation and doing this will have a large impact on RequstUnits charged for an operation and will likely result in queries being throttled sooner. """ try: DeleteContainerIfExists(client, database_id, COLLECTION_ID) database_link = GetDatabaseLink(database_id) # collections = Query_Entities(client, 'collection', parent_link = database_link) # print(collections) # Force a range scan operation on a hash indexed path collection_to_create = { "id" : COLLECTION_ID , "indexingPolicy" : { "includedPaths" : [ {'path' : "/"} ], "excludedPaths" : [ {'path' : "/length/*"} ] # exclude length } } created_Container = client.CreateContainer(database_link, collection_to_create) print(created_Container) print("\n" + "-" * 25 + "\n5. Collection created with index policy") print_dictionary_items(created_Container["indexingPolicy"]) collection_link = GetContainerLink(database_id, COLLECTION_ID) doc1 = client.CreateItem(collection_link, { "id" : "dyn1", "length" : 10, "width" : 5, "height" : 15 }) doc2 = client.CreateItem(collection_link, { "id" : "dyn2", "length" : 7, "width" : 15 }) doc3 = client.CreateItem(collection_link, { "id" : "dyn3", "length" : 2 }) print("Three docs created with ids : ", doc1["id"], doc2["id"], doc3["id"]) # Query for length > 5 - fail, this is a range based query on a Hash index only document query = { "query": "SELECT * FROM r WHERE r.length > 5" } QueryDocumentsWithCustomQuery(client, collection_link, query) # Now add IndexingDirective and repeat query # expect 200 OK because now we are explicitly allowing scans in a query # using the enableScanInQuery directive QueryDocumentsWithCustomQuery(client, collection_link, query) results = list(client.QueryItems(collection_link, query, {"enableScanInQuery" : True})) print("Printing documents queried by range by providing enableScanInQuery = True") for doc in results: print(doc["id"]) # Cleanup client.DeleteContainer(collection_link) print("\n") except errors.HTTPFailure as e: if e.status_code == 409: print("Entity already exists") elif e.status_code == 404: print("Entity doesn't exist") else: raise def UseRangeIndexesOnStrings(client, database_id): """Showing how range queries can be performed even on strings. """ try: DeleteContainerIfExists(client, database_id, COLLECTION_ID) database_link = GetDatabaseLink(database_id) # collections = Query_Entities(client, 'collection', parent_link = database_link) # print(collections) # Use range indexes on strings # This is how you can specify a range index on strings (and numbers) for all properties. # This is the recommended indexing policy for collections. i.e. precision -1 #indexingPolicy = { # 'indexingPolicy': { # 'includedPaths': [ # { # 'indexes': [ # { # 'kind': documents.IndexKind.Range, # 'dataType': documents.DataType.String, # 'precision': -1 # } # ] # } # ] # } #} # For demo purposes, we are going to use the default (range on numbers, hash on strings) for the whole document (/* ) # and just include a range index on strings for the "region". collection_definition = { 'id': COLLECTION_ID, 'indexingPolicy': { 'includedPaths': [ { 'path': '/region/?', 'indexes': [ { 'kind': documents.IndexKind.Range, 'dataType': documents.DataType.String, 'precision': -1 } ] }, { 'path': '/*' } ] } } created_Container = client.CreateContainer(database_link, collection_definition) print(created_Container) print("\n" + "-" * 25 + "\n6. Collection created with index policy") print_dictionary_items(created_Container["indexingPolicy"]) collection_link = GetContainerLink(database_id, COLLECTION_ID) client.CreateItem(collection_link, { "id" : "doc1", "region" : "USA" }) client.CreateItem(collection_link, { "id" : "doc2", "region" : "UK" }) client.CreateItem(collection_link, { "id" : "doc3", "region" : "Armenia" }) client.CreateItem(collection_link, { "id" : "doc4", "region" : "Egypt" }) # Now ordering against region is allowed. You can run the following query query = { "query" : "SELECT * FROM r ORDER BY r.region" } message = "Documents ordered by region" QueryDocumentsWithCustomQuery(client, collection_link, query, message) # You can also perform filters against string comparison like >= 'UK'. Note that you can perform a prefix query, # the equivalent of LIKE 'U%' (is >= 'U' AND < 'U') query = { "query" : "SELECT * FROM r WHERE r.region >= 'U'" } message = "Documents with region begining with U" QueryDocumentsWithCustomQuery(client, collection_link, query, message) # Cleanup client.DeleteContainer(collection_link) print("\n") except errors.HTTPFailure as e: if e.status_code == 409: print("Entity already exists") elif e.status_code == 404: print("Entity doesn't exist") else: raise def PerformIndexTransformations(client, database_id): try: DeleteContainerIfExists(client, database_id, COLLECTION_ID) database_link = GetDatabaseLink(database_id) # collections = Query_Entities(client, 'collection', parent_link = database_link) # print(collections) # Create a collection with default indexing policy created_Container = client.CreateContainer(database_link, {"id" : COLLECTION_ID}) print(created_Container) print("\n" + "-" * 25 + "\n7. Collection created with index policy") print_dictionary_items(created_Container["indexingPolicy"]) # Insert some documents collection_link = GetContainerLink(database_id, COLLECTION_ID) doc1 = client.CreateItem(collection_link, { "id" : "dyn1", "length" : 10, "width" : 5, "height" : 15 }) doc2 = client.CreateItem(collection_link, { "id" : "dyn2", "length" : 7, "width" : 15 }) doc3 = client.CreateItem(collection_link, { "id" : "dyn3", "length" : 2 }) print("Three docs created with ids : ", doc1["id"], doc2["id"], doc3["id"], " with indexing mode", created_Container['indexingPolicy']['indexingMode']) # Switch to use string & number range indexing with maximum precision. print("Changing to string & number range indexing with maximum precision (needed for Order By).") created_Container['indexingPolicy']['includedPaths'][0]['indexes'] = [{ 'kind': documents.IndexKind.Range, 'dataType': documents.DataType.String, 'precision': -1 }] created_Container = client.ReplaceContainer(collection_link, created_Container) # Check progress and wait for completion - should be instantaneous since we have only a few documents, but larger # collections will take time. print_dictionary_items(created_Container["indexingPolicy"]) # Now exclude a path from indexing to save on storage space. print("Now excluding the path /length/ to save on storage space") created_Container['indexingPolicy']['excludedPaths'] = [{"path" : "/length/*"}] created_Container = client.ReplaceContainer(collection_link, created_Container) print_dictionary_items(created_Container["indexingPolicy"]) # Cleanup client.DeleteContainer(collection_link) print("\n") except errors.HTTPFailure as e: if e.status_code == 409: print("Entity already exists") elif e.status_code == 404: print("Entity doesn't exist") else: raise def PerformMultiOrderbyQuery(client, database_id): try: DeleteContainerIfExists(client, database_id, COLLECTION_ID) database_link = GetDatabaseLink(database_id) # Create a collection with composite indexes indexingPolicy = { "compositeIndexes": [ [ { "path": "/numberField", "order": "ascending" }, { "path": "/stringField", "order": "descending" } ], [ { "path": "/numberField", "order": "descending" }, { "path": "/stringField", "order": "ascending" }, { "path": "/numberField2", "order": "descending" }, { "path": "/stringField2", "order": "ascending" } ] ] } container_definition = { 'id': COLLECTION_ID, 'indexingPolicy': indexingPolicy } created_container = client.CreateContainer(database_link, container_definition) print(created_container) print("\n" + "-" * 25 + "\n8. Collection created with index policy") print_dictionary_items(created_container["indexingPolicy"]) # Insert some documents collection_link = GetContainerLink(database_id, COLLECTION_ID) doc1 = client.CreateItem(collection_link, {"id": "doc1", "numberField": 1, "stringField": "1", "numberField2": 1, "stringField2": "1"}) doc2 = client.CreateItem(collection_link, {"id": "doc2", "numberField": 1, "stringField": "1", "numberField2": 1, "stringField2": "2"}) doc3 = client.CreateItem(collection_link, {"id": "doc3", "numberField": 1, "stringField": "1", "numberField2": 2, "stringField2": "1"}) doc4 = client.CreateItem(collection_link, {"id": "doc4", "numberField": 1, "stringField": "1", "numberField2": 2, "stringField2": "2"}) doc5 = client.CreateItem(collection_link, {"id": "doc5", "numberField": 1, "stringField": "2", "numberField2": 1, "stringField2": "1"}) doc6 = client.CreateItem(collection_link, {"id": "doc6", "numberField": 1, "stringField": "2", "numberField2": 1, "stringField2": "2"}) doc7 = client.CreateItem(collection_link, {"id": "doc7", "numberField": 1, "stringField": "2", "numberField2": 2, "stringField2": "1"}) doc8 = client.CreateItem(collection_link, {"id": "doc8", "numberField": 1, "stringField": "2", "numberField2": 2, "stringField2": "2"}) doc9 = client.CreateItem(collection_link, {"id": "doc9", "numberField": 2, "stringField": "1", "numberField2": 1, "stringField2": "1"}) doc10 = client.CreateItem(collection_link, {"id": "doc10", "numberField": 2, "stringField": "1", "numberField2": 1, "stringField2": "2"}) doc11 = client.CreateItem(collection_link, {"id": "doc11", "numberField": 2, "stringField": "1", "numberField2": 2, "stringField2": "1"}) doc12 = client.CreateItem(collection_link, {"id": "doc12", "numberField": 2, "stringField": "1", "numberField2": 2, "stringField2": "2"}) doc13 = client.CreateItem(collection_link, {"id": "doc13", "numberField": 2, "stringField": "2", "numberField2": 1, "stringField2": "1"}) doc14 = client.CreateItem(collection_link, {"id": "doc14", "numberField": 2, "stringField": "2", "numberField2": 1, "stringField2": "2"}) doc15 = client.CreateItem(collection_link, {"id": "doc15", "numberField": 2, "stringField": "2", "numberField2": 2, "stringField2": "1"}) doc16 = client.CreateItem(collection_link, {"id": "doc16", "numberField": 2, "stringField": "2", "numberField2": 2, "stringField2": "2"}) print("Query documents and Order by 1st composite index: Ascending numberField and Descending stringField:") query = { "query": "SELECT * FROM r ORDER BY r.numberField ASC, r.stringField DESC", } QueryDocumentsWithCustomQuery(client, collection_link, query) print("Query documents and Order by inverted 2nd composite index -") print("Ascending numberField, Descending stringField, Ascending numberField2, Descending stringField2") query = { "query": "SELECT * FROM r ORDER BY r.numberField ASC, r.stringField DESC, r.numberField2 ASC, r.stringField2 DESC", } QueryDocumentsWithCustomQuery(client, collection_link, query) # Cleanup client.DeleteContainer(collection_link) print("\n") except errors.HTTPFailure as e: if e.status_code == 409: print("Entity already exists") elif e.status_code == 404: print("Entity doesn't exist") else: raise def RunIndexDemo(): try: client = ObtainClient() FetchAllDatabases(client) # Create database if doesn't exist already. created_db = CreateDatabaseIfNotExists(client, DATABASE_ID) print(created_db) # 1. Exclude a document from the index ExplicitlyExcludeFromIndex(client, DATABASE_ID) # 2. Use manual (instead of automatic) indexing UseManualIndexing(client, DATABASE_ID) # 4. Exclude specified document paths from the index ExcludePathsFromIndex(client, DATABASE_ID) # 5. Force a range scan operation on a hash indexed path RangeScanOnHashIndex(client, DATABASE_ID) # 6. Use range indexes on strings UseRangeIndexesOnStrings(client, DATABASE_ID) # 7. Perform an index transform PerformIndexTransformations(client, DATABASE_ID) # 8. Perform Multi Orderby queries using composite indexes PerformMultiOrderbyQuery(client, DATABASE_ID) except errors.CosmosError as e: raise e if __name__ == '__main__': print("Hello!") for i in [HOST, MASTER_KEY, DATABASE_ID, COLLECTION_ID] : print(i) if test_ssl_connection() == True: RunIndexDemo()azure-cosmos-python-3.1.1/samples/MultiMasterOperations/000077500000000000000000000000001352206500100234325ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/MultiMasterOperations/Configurations.py000066400000000000000000000006221352206500100267760ustar00rootroot00000000000000# Replace ENDPOINT, ACCOUNT_KEY and REGIONS with values from your Azure Cosmos DB account. class Configurations(object): ENDPOINT = "ENDPOINT" ACCOUNT_KEY = "MASTER_KEY" REGIONS = "REGIONS" DATABASE_NAME = "multimaster_demo_db" BASIC_COLLECTION_NAME = "basic_coll" MANUAL_COLLECTION_NAME = "manual_coll" LWW_COLLECTION_NAME = "lww_coll" UDP_COLLECTION_NAME = "udp_coll" azure-cosmos-python-3.1.1/samples/MultiMasterOperations/ConflictWorker.py000066400000000000000000001055431352206500100267470ustar00rootroot00000000000000import uuid import time from multiprocessing.pool import ThreadPool import json import azure.cosmos.errors as errors from azure.cosmos.http_constants import StatusCodes import azure.cosmos.base as base class ConflictWorker(object): def __init__(self, database_name, basic_collection_name, manual_collection_name, lww_collection_name, udp_collection_name): self.clients = [] self.basic_collection_link = "dbs/" + database_name + "/colls/" + basic_collection_name self.manual_collection_link = "dbs/" + database_name + "/colls/" + manual_collection_name self.lww_collection_link = "dbs/" + database_name + "/colls/" + lww_collection_name self.udp_collection_link = "dbs/" + database_name + "/colls/" + udp_collection_name self.database_name = database_name self.basic_collection_name = basic_collection_name self.manual_collection_name = manual_collection_name self.lww_collection_name = lww_collection_name self.udp_collection_name = udp_collection_name def add_client(self, client): self.clients.append(client) def initialize_async(self): create_client = self.clients[0] database = None try: database = create_client.ReadDatabase("dbs/" + self.database_name) except errors.CosmosError as e: if e.status_code == StatusCodes.NOT_FOUND: print("database not found, needs to be created.") else: raise e if not database: database = {'id': self.database_name} database = create_client.CreateDatabase(database) basic_collection = {'id': self.basic_collection_name} basic = self.try_create_document_collection(create_client, database, basic_collection) manual_collection = { 'id': self.manual_collection_name, 'conflictResolutionPolicy': { 'mode': 'Custom' } } manual_collection = self.try_create_document_collection(create_client, database, manual_collection) lww_collection = { 'id': self.lww_collection_name, 'conflictResolutionPolicy': { 'mode': 'LastWriterWins', 'conflictResolutionPath': '/regionId' } } lww_collection = self.try_create_document_collection(create_client, database, lww_collection) udp_collection = { 'id': self.udp_collection_name, 'conflictResolutionPolicy': { 'mode': 'Custom', 'conflictResolutionProcedure': 'dbs/' + self.database_name + "/colls/" + self.udp_collection_name + '/sprocs/resolver' } } udp_collection = self.try_create_document_collection(create_client, database, udp_collection) lww_sproc = {'id':'resolver', 'body': "function resolver(incomingRecord, existingRecord, isTombstone, conflictingRecords) {\r\n" + " var collection = getContext().getCollection();\r\n" + "\r\n" + " if (!incomingRecord) {\r\n" + " if (existingRecord) {\r\n" + "\r\n" + " collection.deleteDocument(existingRecord._self, {}, function(err, responseOptions) {\r\n" + " if (err) throw err;\r\n" + " });\r\n" + " }\r\n" + " } else if (isTombstone) {\r\n" + " // delete always wins.\r\n" + " } else {\r\n" + " var documentToUse = incomingRecord;\r\n" + "\r\n" + " if (existingRecord) {\r\n" + " if (documentToUse.regionId < existingRecord.regionId) {\r\n" + " documentToUse = existingRecord;\r\n" + " }\r\n" + " }\r\n" + "\r\n" + " var i;\r\n" + " for (i = 0; i < conflictingRecords.length; i++) {\r\n" + " if (documentToUse.regionId < conflictingRecords[i].regionId) {\r\n" + " documentToUse = conflictingRecords[i];\r\n" + " }\r\n" + " }\r\n" + "\r\n" + " tryDelete(conflictingRecords, incomingRecord, existingRecord, documentToUse);\r\n" + " }\r\n" + "\r\n" + " function tryDelete(documents, incoming, existing, documentToInsert) {\r\n" + " if (documents.length > 0) {\r\n" + " collection.deleteDocument(documents[0]._self, {}, function(err, responseOptions) {\r\n" + " if (err) throw err;\r\n" + "\r\n" + " documents.shift();\r\n" + " tryDelete(documents, incoming, existing, documentToInsert);\r\n" + " });\r\n" + " } else if (existing) {\r\n" + " collection.replaceDocument(existing._self, documentToInsert,\r\n" + " function(err, documentCreated) {\r\n" + " if (err) throw err;\r\n" + " });\r\n" + " } else {\r\n" + " collection.createDocument(collection.getSelfLink(), documentToInsert,\r\n" + " function(err, documentCreated) {\r\n" + " if (err) throw err;\r\n" + " });\r\n" + " }\r\n" + " }\r\n" + "}" } try: lww_sproc = create_client.CreateStoredProcedure("dbs/" + self.database_name+ "/colls/" + self.udp_collection_name, lww_sproc) except errors.CosmosError as e: if e.status_code == StatusCodes.CONFLICT: return raise e def try_create_document_collection (self, client, database, collection): read_collection = None try: read_collection = client.ReadContainer("dbs/" + database['id'] + "/colls/" + collection['id']) except errors.CosmosError as e: if e.status_code == StatusCodes.NOT_FOUND: print("collection not found, needs to be created.") else: raise errors if read_collection == None: collection['partitionKey'] = {'paths': ['/id'],'kind': 'Hash'} read_collection = client.CreateContainer(database['_self'], collection) print("sleeping for 5 seconds to allow collection create to propagate.") time.sleep(5) return read_collection def run_manual_conflict_async(self): print("\r\nInsert Conflict\r\n") self.run_insert_conflict_on_manual_async() print("\r\nUpdate Conflict\r\n") self.run_update_conflict_on_manual_async() print("\r\nDelete Conflict\r\n") self.run_delete_conflict_on_manual_async() def run_LWW_conflict_async(self): print("\r\nInsert Conflict\r\n") self.run_insert_conflict_on_LWW_async() print("\r\nUpdate Conflict\r\n") self.run_update_conflict_on_LWW_async() print("\r\nDelete Conflict\r\n") self.run_delete_conflict_on_LWW_async() def run_UDP_async(self): print("\r\nInsert Conflict\r\n") self.run_insert_conflict_on_UDP_async() print("\r\nUpdate Conflict\r\n") self.run_update_conflict_on_UDP_async() print("\r\nDelete Conflict\r\n") self.run_delete_conflict_on_UDP_async() def run_insert_conflict_on_manual_async(self): while True: print("1) Performing conflicting insert across %d regions on %s" % (len(self.clients), self.manual_collection_link)) id = str(uuid.uuid4()) i = 0 pool = ThreadPool(processes = len(self.clients)) insert_document_futures = [] for client in self.clients: conflict_document = {'id': id, 'regionId': i, 'regionEndpoint': client.ReadEndpoint} insert_document_future = pool.apply_async(self.try_insert_document, (client, self.manual_collection_link, conflict_document)) insert_document_futures.append(insert_document_future) i += 1 number_of_conflicts = -1 inserted_documents = [] for insert_document_future in insert_document_futures: inserted_document = insert_document_future.get() inserted_documents.append(inserted_document) if inserted_document: number_of_conflicts += 1 if number_of_conflicts > 0: print("2) Caused %d insert conflicts, verifying conflict resolution" % number_of_conflicts) time.sleep(2) #allow conflicts resolution to propagate for conflicting_insert in inserted_documents: if conflicting_insert: self.validate_manual_conflict_async(self.clients, conflicting_insert) break else: print("Retrying insert to induce conflicts") def run_update_conflict_on_manual_async(self): while True: id = str(uuid.uuid4()) conflict_document_for_insertion = {'id': id, 'regionId': 0, 'regionEndpoint': self.clients[0].ReadEndpoint} conflict_document_for_insertion = self.try_insert_document(self.clients[0], self.manual_collection_link, conflict_document_for_insertion) time.sleep(1) #1 Second for write to sync. print("1) Performing conflicting update across %d regions on %s" % (len(self.clients), self.manual_collection_link)); i = 0 options = {'accessCondition': {'condition': 'IfMatch', 'type': conflict_document_for_insertion['_etag']}} pool = ThreadPool(processes = len(self.clients)) update_document_futures = [] for client in self.clients: conflict_document = {'id': id, 'regionId': i, 'regionEndpoint': client.ReadEndpoint} update_document_future = pool.apply_async(self.try_update_document, (client, self.manual_collection_link, conflict_document, options)) update_document_futures.append(update_document_future) i += 1 number_of_conflicts = -1 update_documents = [] for update_document_future in update_document_futures: update_document = update_document_future.get() update_documents.append(update_document) if update_document: number_of_conflicts += 1 if number_of_conflicts > 0: print("2) Caused %d update conflicts, verifying conflict resolution" % number_of_conflicts) time.sleep(2) #allow conflicts resolution to propagate for conflicting_update in update_documents: if conflicting_update: self.validate_manual_conflict_async(self.clients, conflicting_update) break else: print("Retrying update to induce conflicts") def run_delete_conflict_on_manual_async(self): while True: id = str(uuid.uuid4()) conflict_document_for_insertion = {'id': id, 'regionId': 0, 'regionEndpoint': self.clients[0].ReadEndpoint} conflict_document_for_insertion = self.try_insert_document(self.clients[0], self.manual_collection_link, conflict_document_for_insertion) time.sleep(1) #1 Second for write to sync. print("1) Performing conflicting delete across %d regions on %s" % (len(self.clients), self.manual_collection_link)); i = 0 options = {'accessCondition': {'condition': 'IfMatch', 'type': conflict_document_for_insertion['_etag']}} pool = ThreadPool(processes = len(self.clients)) delete_document_futures = [] for client in self.clients: conflict_document = conflict_document_for_insertion.copy() conflict_document['regionId'] = i conflict_document['regionEndpoint'] = client.ReadEndpoint delete_document_future = pool.apply_async(self.try_delete_document, (client, self.manual_collection_link, conflict_document, options)) delete_document_futures.append(delete_document_future) i += 1 number_of_conflicts = -1 delete_documents = [] for delete_document_future in delete_document_futures: delete_document = delete_document_future.get() delete_documents.append(delete_document) if delete_document: number_of_conflicts += 1 if number_of_conflicts > 0: print("2) Caused %d delete conflicts, verifying conflict resolution" % number_of_conflicts) # Conflicts will not be registered in conflict feed for delete-delete # operations. The 'hasDeleteConflict' part of LWW validation can be reused for # manual conflict resolution policy validation of delete-delete conflicts. self.validate_LWW_async(self.clients, delete_documents, True); break else: print("Retrying delete to induce conflicts") def run_insert_conflict_on_LWW_async(self): while True: print("1) Performing conflicting insert across %d regions on %s" % (len(self.clients), self.lww_collection_link)) id = str(uuid.uuid4()) i = 0 pool = ThreadPool(processes = len(self.clients)) insert_document_futures = [] for client in self.clients: conflict_document = {'id': id, 'regionId': i, 'regionEndpoint': client.ReadEndpoint} insert_document_future = pool.apply_async(self.try_insert_document, (client, self.lww_collection_link, conflict_document)) insert_document_futures.append(insert_document_future) i += 1 inserted_documents = [] for insert_document_future in insert_document_futures: inserted_document = insert_document_future.get() if inserted_document: inserted_documents.append(inserted_document) if len(inserted_documents) > 1: print("2) Caused %d insert conflicts, verifying conflict resolution" % len(inserted_documents)) time.sleep(2) #allow conflicts resolution to propagate self.validate_LWW_async(self.clients, inserted_documents, False) break else: print("Retrying insert to induce conflicts") def run_update_conflict_on_LWW_async(self): while True: id = str(uuid.uuid4()) conflict_document_for_insertion = {'id': id, 'regionId': 0, 'regionEndpoint': self.clients[0].ReadEndpoint} conflict_document_for_insertion = self.try_insert_document(self.clients[0], self.lww_collection_link, conflict_document_for_insertion) time.sleep(1) #1 Second for write to sync. print("1) Performing conflicting update across %d regions on %s" % (len(self.clients), self.lww_collection_link)); i = 0 options = {'accessCondition': {'condition': 'IfMatch', 'type': conflict_document_for_insertion['_etag']}} pool = ThreadPool(processes = len(self.clients)) update_document_futures = [] for client in self.clients: conflict_document = {'id': id, 'regionId': i, 'regionEndpoint': client.ReadEndpoint} update_document_future = pool.apply_async(self.try_update_document, (client, self.lww_collection_link, conflict_document, options)) update_document_futures.append(update_document_future) i += 1 update_documents = [] for update_document_future in update_document_futures: update_document = update_document_future.get() if update_document: update_documents.append(update_document) if len(update_documents) > 1: print("2) Caused %d update conflicts, verifying conflict resolution" % len(update_documents)) time.sleep(2) #allow conflicts resolution to propagate self.validate_LWW_async(self.clients, update_documents, False) break else: print("Retrying update to induce conflicts") def run_delete_conflict_on_LWW_async(self): while True: id = str(uuid.uuid4()) conflict_document_for_insertion = {'id': id, 'regionId': 0, 'regionEndpoint': self.clients[0].ReadEndpoint} conflict_document_for_insertion = self.try_insert_document(self.clients[0], self.lww_collection_link, conflict_document_for_insertion) time.sleep(1) #1 Second for write to sync. print("1) Performing conflicting update/delete across 3 regions on %s" % self.lww_collection_link) i = 0 options = {'accessCondition': {'condition': 'IfMatch', 'type': conflict_document_for_insertion['_etag']}} pool = ThreadPool(processes = len(self.clients)) delete_document_futures = [] for client in self.clients: conflict_document = {'id': id, 'regionId': i, 'regionEndpoint': client.ReadEndpoint} delete_document_future = pool.apply_async(self.try_update_or_delete_document, (client, self.lww_collection_link, conflict_document, options)) delete_document_futures.append(delete_document_future) i += 1 delete_documents = [] for delete_document_future in delete_document_futures: delete_document = delete_document_future.get() if delete_document: delete_documents.append(delete_document) if len(delete_documents) > 1: print("2) Caused %d delete conflicts, verifying conflict resolution" % len(delete_documents)) time.sleep(2) #allow conflicts resolution to propagate # Delete should always win. irrespective of UDP. self.validate_LWW_async(self.clients, delete_documents, True) break else: print("Retrying update/delete to induce conflicts") def run_insert_conflict_on_UDP_async(self): while True: print("1) Performing conflicting insert across 3 regions on %s" % self.udp_collection_link) id = str(uuid.uuid4()) i = 0 pool = ThreadPool(processes = len(self.clients)) insert_document_futures = [] for client in self.clients: conflict_document = {'id': id, 'regionId': i, 'regionEndpoint': client.ReadEndpoint} insert_document_future = pool.apply_async(self.try_insert_document, (client, self.udp_collection_link, conflict_document)) insert_document_futures.append(insert_document_future) i += 1 inserted_documents = [] for insert_document_future in insert_document_futures: inserted_document = insert_document_future.get() if inserted_document: inserted_documents.append(inserted_document) if len(inserted_documents) > 1: print("2) Caused %d insert conflicts, verifying conflict resolution" % len(inserted_documents)) time.sleep(2) #allow conflicts resolution to propagate self.validate_UDP_async(self.clients, inserted_documents, False) break else: print("Retrying insert to induce conflicts") def run_update_conflict_on_UDP_async(self): while True: id = str(uuid.uuid4()) conflict_document_for_insertion = {'id': id, 'regionId': 0, 'regionEndpoint': self.clients[0].ReadEndpoint} conflict_document_for_insertion = self.try_insert_document(self.clients[0], self.udp_collection_link, conflict_document_for_insertion) time.sleep(1) #1 Second for write to sync. print("1) Performing conflicting update across %d regions on %s" % (len(self.clients), self.udp_collection_link)); i = 0 options = {'accessCondition': {'condition': 'IfMatch', 'type': conflict_document_for_insertion['_etag']}} pool = ThreadPool(processes = len(self.clients)) update_document_futures = [] for client in self.clients: conflict_document = {'id': id, 'regionId': i, 'regionEndpoint': client.ReadEndpoint} update_document_future = pool.apply_async(self.try_update_document, (client, self.udp_collection_link, conflict_document, options)) update_document_futures.append(update_document_future) i += 1 update_documents = [] for update_document_future in update_document_futures: update_document = update_document_future.get() if update_document: update_documents.append(update_document) if len(update_documents) > 1: print("2) Caused %d update conflicts, verifying conflict resolution" % len(update_documents)) time.sleep(2) #allow conflicts resolution to propagate self.validate_UDP_async(self.clients, update_documents, False) break else: print("Retrying update to induce conflicts") def run_delete_conflict_on_UDP_async(self): while True: id = str(uuid.uuid4()) conflict_document_for_insertion = {'id': id, 'regionId': 0, 'regionEndpoint': self.clients[0].ReadEndpoint} conflict_document_for_insertion = self.try_insert_document(self.clients[0], self.udp_collection_link, conflict_document_for_insertion) time.sleep(1) #1 Second for write to sync. print("1) Performing conflicting update/delete across 3 regions on %s" % self.udp_collection_link) i = 0 options = {'accessCondition': {'condition': 'IfMatch', 'type': conflict_document_for_insertion['_etag']}} pool = ThreadPool(processes = len(self.clients)) delete_document_futures = [] for client in self.clients: conflict_document = {'id': id, 'regionId': i, 'regionEndpoint': client.ReadEndpoint} delete_document_future = pool.apply_async(self.try_update_or_delete_document, (client, self.udp_collection_link, conflict_document, options)) delete_document_futures.append(delete_document_future) i += 1 delete_documents = [] for delete_document_future in delete_document_futures: delete_document = delete_document_future.get() if delete_document: delete_documents.append(delete_document) if len(delete_documents) > 1: print("2) Caused %d delete conflicts, verifying conflict resolution" % len(delete_documents)) time.sleep(2) #allow conflicts resolution to propagate # Delete should always win. irrespective of UDP. self.validate_UDP_async(self.clients, delete_documents, True) break else: print("Retrying update/delete to induce conflicts") def try_insert_document(self, client, collection_uri, document): try: return client.CreateItem(collection_uri, document) except errors.CosmosError as e: if e.status_code == StatusCodes.CONFLICT: return None raise e def try_update_document(self, client, collection_uri, document, options): try: options['partitionKey'] = document['id'] return client.ReplaceItem(collection_uri + "/docs/" + document['id'], document, options); except errors.CosmosError as e: if (e.status_code == StatusCodes.PRECONDITION_FAILED or e.status_code == StatusCodes.NOT_FOUND): # Lost synchronously or no document yet. No conflict is induced. return None raise e def try_delete_document(self, client, collection_uri, document, options): try: options['partitionKey'] = document['id'] client.DeleteItem(collection_uri + "/docs/" + document['id'], options) return document except errors.CosmosError as e: if (e.status_code == StatusCodes.PRECONDITION_FAILED or e.status_code == StatusCodes.NOT_FOUND): #Lost synchronously. No conflict is induced. return None raise e def try_update_or_delete_document(self, client, collection_uri, conflict_document, options): if int(conflict_document['regionId']) % 2 == 1: #We delete from region 1, even though region 2 always win. return self.try_delete_document(client, collection_uri, conflict_document, options) else: return self.try_update_document(client, collection_uri, conflict_document, options) def validate_manual_conflict_async(self, clients, conflict_document): conflict_exists = False for client in clients: conflict_exists = self.validate_manual_conflict_async_internal(client, conflict_document) if conflict_exists: self.delete_conflict_async(conflict_document) def validate_manual_conflict_async_internal(self, client, conflict_document): while True: conflicts_iterartor = iter(client.ReadConflicts(self.manual_collection_link)) conflict = next(conflicts_iterartor, None) while conflict: if conflict['operationType'] != 'delete': conflict_document_content = json.loads(conflict['content']) if conflict_document['id'] == conflict_document_content['id']: if ((conflict_document['_rid'] == conflict_document_content['_rid']) and (conflict_document['_etag'] == conflict_document_content['_etag'])): print("Document from Region %d lost conflict @ %s" % (int(conflict_document['regionId']), client.ReadEndpoint)) return True else: #Checking whether this is the winner. options = {'partitionKey': conflict_document['id']} winner_document = client.ReadItem(conflict_document['_self'], options) print("Document from Region %d won the conflict @ %s" % (int(winner_document['regionId']), client.ReadEndpoint)) return False else: if conflict['resourceId'] == conflict_document['_rid']: print("Delete conflict found @ %s" % client.ReadEndpoint) return False conflict = next(conflicts_iterartor, None) self.trace_error("Document %s is not found in conflict feed @ %s, retrying" % (conflict_document['id'], client.ReadEndpoint)) time.sleep(0.5) def delete_conflict_async(self, conflict_document): del_client = self.clients[0] conflicts_iterartor = iter(del_client.ReadConflicts(self.manual_collection_link)) conflict = next(conflicts_iterartor, None) while conflict: conflict_content = json.loads(conflict['content']) options = {'partitionKey': conflict_content['id']} if conflict['operationType'] != 'delete': if ((conflict_content['_rid'] == conflict_document['_rid']) and (conflict_content['_etag'] == conflict_document['_etag'])): print("Deleting manual conflict %s from region %d" % (conflict['resourceId'], int(conflict_content['regionId']))) del_client.DeleteConflict(conflict['_self'], options) elif conflict['resourceId'] == conflict_document['_rid']: print("Deleting manual conflict %s from region %d" % (conflict['resourceId'], int(conflict_document['regionId']))) del_client.DeleteConflict(conflict['_self'], options) conflict = next(conflicts_iterartor, None) def validate_LWW_async(self, clients, conflict_document, has_delete_conflict): for client in clients: self.validate_LWW_async_internal(client, conflict_document, has_delete_conflict) def validate_LWW_async_internal(self, client, conflict_document, has_delete_conflict): conflicts_iterartor =iter(client.ReadConflicts(self.lww_collection_link)) conflict = next(conflicts_iterartor, None) conflict_count = 0 while conflict: conflict_count += 1 conflict = next(conflicts_iterartor, None) if conflict_count > 0: self.trace_error("Found %d conflicts in the lww collection" % conflict_count) return if has_delete_conflict: while True: try: options = {'partitionKey': conflict_document[0]['id']} client.ReadItem(conflict_document[0]['_self'], options) self.trace_error("Delete conflict for document %s didnt win @ %s" % (conflict_document[0]['id'], client.ReadEndpoint)) time.sleep(0.5) except errors.CosmosError as e: if e.status_code == StatusCodes.NOT_FOUND: print("Delete conflict won @ %s" % client.ReadEndpoint) return else: self.trace_error("Delete conflict for document %s didnt win @ %s" % (conflict_document[0]['id'], client.ReadEndpoint)) time.sleep(0.5) winner_document = None for document in conflict_document: if winner_document is None or int(winner_document['regionId']) <= int(document['regionId']): winner_document = document print("Document from region %d should be the winner" % int(winner_document['regionId'])) while True: try: options = {'partitionKey': winner_document['id']} existing_document = client.ReadItem(winner_document['_self'], options) if int(existing_document['regionId']) == int(winner_document['regionId']): print("Winner document from region %d found at %s" % (int(existing_document['regionId']), client.ReadEndpoint)) break else: self.trace_error("Winning document version from region %d is not found @ %s, retrying..." % (int(winner_document["regionId"]), client.WriteEndpoint)) time.sleep(0.5) except errors.CosmosError as e: self.trace_error("Winner document from region %d is not found @ %s, retrying..." % (int(winner_document["regionId"]), client.WriteEndpoint)) time.sleep(0.5) def validate_UDP_async(self, clients, conflict_document, has_delete_conflict): for client in clients: self.validate_UDP_async_internal(client, conflict_document, has_delete_conflict) def validate_UDP_async_internal(self, client, conflict_document, has_delete_conflict): conflicts_iterartor = iter(client.ReadConflicts(self.udp_collection_link)) conflict = next(conflicts_iterartor, None) conflict_count = 0 while conflict: conflict_count += 1 conflict = next(conflicts_iterartor, None) if conflict_count > 0: self.trace_error("Found %d conflicts in the udp collection" % conflictCount) return if has_delete_conflict: while True: try: options = {'partitionKey': conflict_document[0]['id']} client.ReadItem(conflict_document[0]['_self'], options) self.trace_error("Delete conflict for document %s didnt win @ %s" % (conflict_document[0]['id'], client.ReadEndpoint)) time.sleep(0.5) except errors.CosmosError as e: if e.status_code == StatusCodes.NOT_FOUND: print("Delete conflict won @ %s" % client.ReadEndpoint) return else: self.trace_error("Delete conflict for document %s didnt win @ %s" % (conflict_document[0]['id'], client.ReadEndpoint)) time.sleep(0.5) winner_document = None for document in conflict_document: if winner_document is None or int(winner_document['regionId']) <= int(document['regionId']): winner_document = document; print("Document from region %d should be the winner" % int(winner_document['regionId'])) while True: try: options = {'partitionKey': winner_document['id']} existing_document = client.ReadItem(self.udp_collection_link + "/docs/" + winner_document['id'], options) if int(existing_document['regionId']) == int(winner_document['regionId']): print("Winner document from region %d found at %s" % (int(existing_document["regionId"]), client.ReadEndpoint)) break else: self.trace_error("Winning document version from region %d is not found @ %s, retrying..." % (int(winner_document['regionId']), client.WriteEndpoint)) time.sleep(0.5) except errors.CosmosError as e: self.trace_error("Winner document from region %d is not found @ %s, retrying..." % (int(winner_document['regionId']), client.WriteEndpoint)) time.sleep(0.5) def trace_error(self, message): print('\n' + message + '\n') azure-cosmos-python-3.1.1/samples/MultiMasterOperations/MultiMasterOperations.pyproj000066400000000000000000000041021352206500100312060ustar00rootroot00000000000000 Debug 2.0 f29d7a3e-cb9a-4b15-bda1-bb30f3ca9cfa . Program.py . . MultiMasterOperations MultiMasterOperations Global|PythonCore|3.4 true false true false Code Code Code Code Code azure-cosmos-python-3.1.1/samples/MultiMasterOperations/MultiMasterScenario.py000066400000000000000000000076611352206500100277500ustar00rootroot00000000000000from Configurations import Configurations from ConflictWorker import ConflictWorker from Worker import Worker from multiprocessing.pool import ThreadPool import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client class MultiMasterScenario(object): def __init__(self): self.account_endpoint = Configurations.ENDPOINT self.account_key = Configurations.ACCOUNT_KEY self.regions = Configurations.REGIONS.split(';') self.database_name = Configurations.DATABASE_NAME self.manual_collection_name = Configurations.MANUAL_COLLECTION_NAME self.lww_collection_name = Configurations.LWW_COLLECTION_NAME self.udp_collection_name = Configurations.UDP_COLLECTION_NAME self.basic_collection_name = Configurations.BASIC_COLLECTION_NAME self.workers = [] self.conflict_worker = ConflictWorker(self.database_name, self.basic_collection_name, self.manual_collection_name, self.lww_collection_name, self.udp_collection_name) self.pool = ThreadPool(processes = len(self.regions)) for region in self.regions: connection_policy = documents.ConnectionPolicy() connection_policy.UseMultipleWriteLocations = True connection_policy.PreferredLocations = [region] client = cosmos_client.CosmosClient(self.account_endpoint, {'masterKey': self.account_key}, connection_policy, documents.ConsistencyLevel.Session) self.workers.append(Worker(client, self.database_name, self.basic_collection_name)) self.conflict_worker.add_client(client) def initialize_async(self): self.conflict_worker.initialize_async() print("Initialized collections.") def run_basic_async(self): print("\n####################################################") print("Basic Active-Active") print("####################################################") print("1) Starting insert loops across multiple regions ...") documents_to_insert_per_worker = 100 run_loop_futures = [] for worker in self.workers: run_loop_future = self.pool.apply_async(worker.run_loop_async, (documents_to_insert_per_worker,)) run_loop_futures.append(run_loop_future) for run_loop_future in run_loop_futures: run_loop_future.get() print("2) Reading from every region ...") expected_documents = len(self.workers) * documents_to_insert_per_worker read_all_futures = [] for worker in self.workers: read_all_future = self.pool.apply_async(worker.read_all_async, (expected_documents,)) read_all_futures.append(read_all_future) for read_all_future in read_all_futures: read_all_future.get() print("3) Deleting all the documents ...") self.workers[0].delete_all_async() print("####################################################") def run_manual_conflict_async(self): print("\n####################################################") print("Manual Conflict Resolution") print("####################################################") self.conflict_worker.run_manual_conflict_async() print("####################################################") def run_LWW_async(self): print("\n####################################################") print("LWW Conflict Resolution") print("####################################################") self.conflict_worker.run_LWW_conflict_async() print("####################################################") def run_UDP_async(self): print("\n####################################################") print("UDP Conflict Resolution") print("####################################################") self.conflict_worker.run_UDP_async() print("####################################################") azure-cosmos-python-3.1.1/samples/MultiMasterOperations/Program.py000066400000000000000000000004761352206500100254220ustar00rootroot00000000000000from MultiMasterScenario import MultiMasterScenario if __name__ == '__main__': print("Multimaster demo started!") scenario = MultiMasterScenario() scenario.initialize_async() scenario.run_basic_async() scenario.run_manual_conflict_async() scenario.run_LWW_async() scenario.run_UDP_async() azure-cosmos-python-3.1.1/samples/MultiMasterOperations/Worker.py000066400000000000000000000050151352206500100252560ustar00rootroot00000000000000import uuid import time import azure.cosmos.errors as errors from azure.cosmos.http_constants import StatusCodes class Worker(object): def __init__(self, client, database_name, collection_name): self.client = client self.document_collection_link = "dbs/" + database_name + "/colls/" + collection_name def run_loop_async(self, documents_to_insert): iteration_count = 0 latency = [] while iteration_count < documents_to_insert: document = {'id': str(uuid.uuid4())} iteration_count += 1 start = int(round(time.time() * 1000)) self.client.CreateItem(self.document_collection_link, document) end = int(round(time.time() * 1000)) latency.append(end - start) latency = sorted(latency) p50_index = int(len(latency) / 2) print("Inserted %d documents at %s with p50 %d ms" % (documents_to_insert, self.client.WriteEndpoint, latency[p50_index])) return document def read_all_async(self, expected_number_of_documents): while True: total_item_read = 0 query_iterable = self.client.ReadItems(self.document_collection_link) it = iter(query_iterable) doc = next(it, None) while doc: total_item_read += 1 doc = next(it, None) if total_item_read < expected_number_of_documents: print("Total item read %d from %s is less than %d, retrying reads" % (total_item_read, self.client.WriteEndpoint, expected_number_of_documents)) time.sleep(1) continue else: print("Read %d items from %s" % (total_item_read, self.client.ReadEndpoint)) break def delete_all_async(self): query_iterable = self.client.ReadItems(self.document_collection_link) it = iter(query_iterable) doc = next(it, None) while doc: try: self.client.DeleteItem(doc['_self'], {'partitionKey': doc['id']}) except errors.CosmosError as e: if e.status_code != StatusCodes.NOT_FOUND: print("Error occurred while deleting document from %s" % self.client.WriteEndpoint) else: raise e doc = next(it, None) print("Deleted all documents from region %s" % self.client.WriteEndpoint)azure-cosmos-python-3.1.1/samples/Shared/000077500000000000000000000000001352206500100203265ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/Shared/PossibleOptions.txt000066400000000000000000000004511352206500100242230ustar00rootroot00000000000000continuation preTriggerInclude postTriggerInclude maxItemCount indexingDirective consistencyLevel sessionToken enableScanInQuery resourceTokenExpirySeconds offerType offerThroughput partitionKey enableCrossPartitionQuery enableScriptLogging offerEnableRUPerMinuteThroughput disableRUPerMinuteUsageazure-cosmos-python-3.1.1/samples/Shared/__init__.py000066400000000000000000000000001352206500100224250ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/Shared/config.py000066400000000000000000000002111352206500100221370ustar00rootroot00000000000000settings = { 'host': '[YOUR ENDPOINT]', 'master_key': '[YOUR KEY]', 'database_id': 'pysamples', 'collection_id': 'data' }azure-cosmos-python-3.1.1/samples/__init__.py000066400000000000000000000000001352206500100212170ustar00rootroot00000000000000azure-cosmos-python-3.1.1/samples/readme.md000066400000000000000000000000001352206500100206650ustar00rootroot00000000000000azure-cosmos-python-3.1.1/setup.py000066400000000000000000000021401352206500100171630ustar00rootroot00000000000000#!/usr/bin/env python from distutils.core import setup import setuptools setup(name='azure-cosmos', version='3.1.1', description='Azure Cosmos Python SDK', author="Microsoft", author_email="askdocdb@microsoft.com", maintainer="Microsoft", maintainer_email="askdocdb@microsoft.com", url="https://github.com/Azure/azure-documentdb-python", license='MIT', install_requires=['six >=1.6', 'requests>=2.10.0'], classifiers=[ 'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Software Development :: Libraries :: Python Modules' ], packages=setuptools.find_packages(exclude=['test', 'test.*'])) azure-cosmos-python-3.1.1/test-requirements.txt000066400000000000000000000000321352206500100217100ustar00rootroot00000000000000-r requirements.txt pytestazure-cosmos-python-3.1.1/test/000077500000000000000000000000001352206500100164335ustar00rootroot00000000000000azure-cosmos-python-3.1.1/test/BaselineTest.PathParser.json000066400000000000000000000046451352206500100237710ustar00rootroot00000000000000[ { "path": "/", "parts": [ ] }, { "path": "/*", "parts": [ "*" ] }, { "path": "/\"Key1\"/*", "parts": [ "Key1", "*" ] }, { "path": "/\"Key1\"/\"StringValue\"/*", "parts": [ "Key1", "StringValue", "*" ] }, { "path": "/'Key1'/'StringValue'/*", "parts": [ "Key1", "StringValue", "*" ] }, { "path": "/'Ke\\\"\\\"y1'/'Strin\\\"gValue'/*", "parts": [ "Ke\\\"\\\"y1", "Strin\\\"gValue", "*" ] }, { "path": "/'Ke\\\"\\\"y1'/\"Strin'gValue\"/*", "parts": [ "Ke\\\"\\\"y1", "Strin'gValue", "*" ] }, { "path": "/'Key1'/'StringValue'/*", "parts": [ "Key1", "StringValue", "*" ] }, { "path": "/\"Key1\"/\"Key2\"/*", "parts": [ "Key1", "Key2", "*" ] }, { "path": "/\"Key1\"/\"Key2\"/\"Key3\"/*", "parts": [ "Key1", "Key2", "Key3", "*" ] }, { "path": "/\"A\"/\"B\"/\"R\"/[]/\"Address\"/[]/*", "parts": [ "A", "B", "R", "[]", "Address", "[]", "*" ] }, { "path": "/\"A\"/\"B\"/\"R\"/[]/\"Address\"/[]/*", "parts": [ "A", "B", "R", "[]", "Address", "[]", "*" ] }, { "path": "/\"A\"/\"B\"/\"R\"/[]/\"Address\"/*", "parts": [ "A", "B", "R", "[]", "Address", "*" ] }, { "path": "/\"Key1\"/\"Key2\"/?", "parts": [ "Key1", "Key2", "?" ] }, { "path": "/\"Key1\"/\"Key2\"/*", "parts": [ "Key1", "Key2", "*" ] }, { "path": "/\"123\"/\"StringValue\"/*", "parts": [ "123", "StringValue", "*" ] }, { "path": "/'!@#$%^&*()_+='/'StringValue'/*", "parts": [ "!@#$%^&*()_+=", "StringValue", "*" ] }, { "path": "/\"_ts\"/?", "parts": [ "_ts", "?" ] }, { "path": "/[]/\"City\"/*", "parts": [ "[]", "City", "*" ] }, { "path": "/[]/*", "parts": [ "[]", "*" ] }, { "path": "/[]/\"fine!\"/*", "parts": [ "[]", "fine!", "*" ] }, { "path": "/\"this is a long key with speicial characters (*)(*)__)((*&*(&*&'*(&)()(*_)()(_(_)*!@#$%^ and numbers 132654890\"/*", "parts": [ "this is a long key with speicial characters (*)(*)__)((*&*(&*&'*(&)()(*_)()(_(_)*!@#$%^ and numbers 132654890", "*" ] }, { "path": "/ Key 1 / Key 2 ", "parts": [ "Key 1", "Key 2" ] } ] azure-cosmos-python-3.1.1/test/__init__.py000066400000000000000000000021171352206500100205450ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE.azure-cosmos-python-3.1.1/test/aggregate_tests.py000066400000000000000000000251521352206500100221620ustar00rootroot00000000000000# The MIT License (MIT) # Copyright (c) 2017 Microsoft Corporation # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import print_function import unittest import uuid import pytest from six import with_metaclass from six.moves import xrange import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents import test.test_config as test_config from azure.cosmos.errors import HTTPFailure class _config: host = test_config._test_config.host master_key = test_config._test_config.masterKey connection_policy = test_config._test_config.connectionPolicy PARTITION_KEY = 'key' UNIQUE_PARTITION_KEY = 'uniquePartitionKey' FIELD = 'field' DOCUMENTS_COUNT = 400 DOCS_WITH_SAME_PARTITION_KEY = 200 docs_with_numeric_id = 0 sum = 0 class AggregateQueryTestSequenceMeta(type): def __new__(mcs, name, bases, dict): def _run_one(query, expected_result): def test(self): self._execute_query_and_validate_results(mcs.client, mcs.collection_link, query, expected_result) return test def _setup(): if (not _config.master_key or not _config.host): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") mcs.client = cosmos_client.CosmosClient(_config.host, {'masterKey': _config.master_key}, _config.connection_policy) created_db = test_config._test_config.create_database_if_not_exist(mcs.client) created_collection = _create_collection(mcs.client, created_db) mcs.collection_link = _get_collection_link(created_db, created_collection) # test documents document_definitions = [] values = [None, False, True, "abc", "cdfg", "opqrs", "ttttttt", "xyz", "oo", "ppp"] for value in values: d = {_config.PARTITION_KEY: value} document_definitions.append(d) for i in xrange(_config.DOCS_WITH_SAME_PARTITION_KEY): d = {_config.PARTITION_KEY: _config.UNIQUE_PARTITION_KEY, 'resourceId': i, _config.FIELD: i + 1} document_definitions.append(d) _config.docs_with_numeric_id = \ _config.DOCUMENTS_COUNT - len(values) - _config.DOCS_WITH_SAME_PARTITION_KEY for i in xrange(_config.docs_with_numeric_id): d = {_config.PARTITION_KEY: i + 1} document_definitions.append(d) _config.sum = _config.docs_with_numeric_id \ * (_config.docs_with_numeric_id + 1) / 2.0 _insert_doc(mcs.collection_link, document_definitions, mcs.client) def _generate_test_configs(): aggregate_query_format = 'SELECT VALUE {}(r.{}) FROM r WHERE {}' aggregate_orderby_query_format = 'SELECT VALUE {}(r.{}) FROM r WHERE {} ORDER BY r.{}' aggregate_configs = [ ['AVG', _config.sum / _config.docs_with_numeric_id, 'IS_NUMBER(r.{})'.format(_config.PARTITION_KEY)], ['AVG', None, 'true'], ['COUNT', _config.DOCUMENTS_COUNT, 'true'], ['MAX', 'xyz', 'true'], ['MIN', None, 'true'], ['SUM', _config.sum, 'IS_NUMBER(r.{})'.format(_config.PARTITION_KEY)], ['SUM', None, 'true'] ] for operator, expected, condition in aggregate_configs: _all_tests.append([ '{} {}'.format(operator, condition), aggregate_query_format.format(operator, _config.PARTITION_KEY, condition), expected]) _all_tests.append([ '{} {} OrderBy'.format(operator, condition), aggregate_orderby_query_format.format(operator, _config.PARTITION_KEY, condition, _config.PARTITION_KEY), expected]) aggregate_single_partition_format = 'SELECT VALUE {}(r.{}) FROM r WHERE r.{} = \'{}\'' aggregate_orderby_single_partition_format = 'SELECT {}(r.{}) FROM r WHERE r.{} = \'{}\'' same_partiton_sum = _config.DOCS_WITH_SAME_PARTITION_KEY * (_config.DOCS_WITH_SAME_PARTITION_KEY + 1) / 2.0 aggregate_single_partition_configs = [ ['AVG', same_partiton_sum / _config.DOCS_WITH_SAME_PARTITION_KEY], ['COUNT', _config.DOCS_WITH_SAME_PARTITION_KEY], ['MAX', _config.DOCS_WITH_SAME_PARTITION_KEY], ['MIN', 1], ['SUM', same_partiton_sum] ] for operator, expected in aggregate_single_partition_configs: _all_tests.append([ '{} SinglePartition {}'.format(operator, 'SELECT VALUE'), aggregate_single_partition_format.format( operator, _config.FIELD, _config.PARTITION_KEY, _config.UNIQUE_PARTITION_KEY), expected]) _all_tests.append([ '{} SinglePartition {}'.format(operator, 'SELECT'), aggregate_orderby_single_partition_format.format( operator, _config.FIELD, _config.PARTITION_KEY, _config.UNIQUE_PARTITION_KEY), Exception()]) def _run_all(): for test_name, query, expected_result in _all_tests: test_name = "test_%s" % test_name dict[test_name] = _run_one(query, expected_result) def _create_collection(client, created_db): collection_definition = { 'id': 'aggregate tests collection ' + str(uuid.uuid4()), 'indexingPolicy': { 'includedPaths': [ { 'path': '/', 'indexes': [ { 'kind': 'Range', 'dataType': 'Number' }, { 'kind': 'Range', 'dataType': 'String' } ] } ] }, 'partitionKey': { 'paths': [ '/{}'.format(_config.PARTITION_KEY) ], 'kind': documents.PartitionKind.Hash } } collection_options = {'offerThroughput': 10100} created_collection = client.CreateContainer(_get_database_link(created_db), collection_definition, collection_options) return created_collection def _insert_doc(collection_link, document_definitions, client): created_docs = [] for d in document_definitions: created_doc = client.CreateItem(collection_link, d) created_docs.append(created_doc) return created_docs def _get_database_link(database, is_name_based=True): if is_name_based: return 'dbs/' + database['id'] else: return database['_self'] def _get_collection_link(database, document_collection, is_name_based=True): if is_name_based: return _get_database_link(database) + '/colls/' + document_collection['id'] else: return document_collection['_self'] _all_tests = [] _setup() _generate_test_configs() _run_all() return type.__new__(mcs, name, bases, dict) @pytest.mark.usefixtures("teardown") class AggregationQueryTest(with_metaclass(AggregateQueryTestSequenceMeta, unittest.TestCase)): def _execute_query_and_validate_results(self, client, collection_link, query, expected): print('Running test with query: ' + query) # executes the query and validates the results against the expected results options = {'enableCrossPartitionQuery': 'true'} result_iterable = client.QueryItems(collection_link, query, options) def _verify_result(): ###################################### # test next() behavior ###################################### it = result_iterable.__iter__() def invokeNext(): return next(it) # validate that invocations of next() produces the same results as expected item = invokeNext() self.assertEqual(item, expected) # after the result set is exhausted, invoking next must raise a StopIteration exception self.assertRaises(StopIteration, invokeNext) ###################################### # test fetch_next_block() behavior ###################################### fetched_res = result_iterable.fetch_next_block() fetched_size = len(fetched_res) self.assertEqual(fetched_size, 1) self.assertEqual(fetched_res[0], expected) # no more results will be returned self.assertEqual(result_iterable.fetch_next_block(), []) if isinstance(expected, Exception): self.assertRaises(HTTPFailure, _verify_result) else: _verify_result() if __name__ == "__main__": unittest.main() azure-cosmos-python-3.1.1/test/base_unit_tests.py000066400000000000000000000006471352206500100222070ustar00rootroot00000000000000import unittest import pytest import azure.cosmos.base as base @pytest.mark.usefixtures("teardown") class BaseUnitTests(unittest.TestCase): def test_is_name_based(self): self.assertFalse(base.IsNameBased("dbs/xjwmAA==/")) # This is a database name that ran into 'Incorrect padding' # exception within base.IsNameBased function self.assertTrue(base.IsNameBased("dbs/paas_cmr")) azure-cosmos-python-3.1.1/test/conftest.py000066400000000000000000000040601352206500100206320ustar00rootroot00000000000000# The MIT License (MIT) # Copyright (c) 2017 Microsoft Corporation # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE # pytest fixture 'teardown' is called at the end of a test run to clean up resources import pytest import azure.cosmos.cosmos_client as cosmos_client import test.test_config as test_config import azure.cosmos.errors as errors from azure.cosmos.http_constants import StatusCodes @pytest.fixture(scope="session") def teardown(request): def delete_database(): print("Cleaning up test resources...") config = test_config._test_config host = config.host masterKey = config.masterKey connectionPolicy = config.connectionPolicy client = cosmos_client.CosmosClient(host, {'masterKey': masterKey}, connectionPolicy) try: client.DeleteDatabase("dbs/" + test_config._test_config.TEST_DATABASE_ID) except errors.HTTPFailure as e: if e.status_code != StatusCodes.NOT_FOUND: raise e print("Clean up completed!") request.addfinalizer(delete_database) return None azure-cosmos-python-3.1.1/test/crud_tests.py000066400000000000000000005413721352206500100212000ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """End to end test. """ import json import logging import os.path import sys import unittest from six.moves import xrange from struct import unpack, pack # from six.moves.builtins import * import time import six if six.PY2: import urllib as urllib else: import urllib.parse as urllib import uuid import pytest import azure.cosmos.base as base import azure.cosmos.consistent_hash_ring as consistent_hash_ring import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.errors as errors import azure.cosmos.hash_partition_resolver as hash_partition_resolver from azure.cosmos.http_constants import HttpHeaders, StatusCodes, SubStatusCodes import azure.cosmos.murmur_hash as murmur_hash import azure.cosmos.range_partition_resolver as range_partition_resolver import azure.cosmos.range as partition_range import test.test_config as test_config import test.test_partition_resolver as test_partition_resolver import azure.cosmos.base as base #IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos account. # Collections are billing entities. By running these test cases, you may incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class CRUDTests(unittest.TestCase): """Python CRUD Tests. """ configs = test_config._test_config host = configs.host masterKey = configs.masterKey connectionPolicy = configs.connectionPolicy client = cosmos_client.CosmosClient(host, {'masterKey': masterKey}, connectionPolicy) databseForTest = configs.create_database_if_not_exist(client) def __AssertHTTPFailureWithStatus(self, status_code, func, *args, **kwargs): """Assert HTTP failure with status. :Parameters: - `status_code`: int - `func`: function """ try: func(*args, **kwargs) self.assertFalse(True, 'function should fail.') except errors.HTTPFailure as inst: self.assertEqual(inst.status_code, status_code) @classmethod def setUpClass(cls): if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_ENDPOINT_HERE]'): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") def setUp(self): self.client = cosmos_client.CosmosClient(self.host, {'masterKey': self.masterKey}, self.connectionPolicy) def test_database_crud_self_link(self): self._test_database_crud(False) def test_database_crud_name_based(self): self._test_database_crud(True) def _test_database_crud(self, is_name_based): # read databases. databases = list(self.client.ReadDatabases()) # create a database. before_create_databases_count = len(databases) database_definition = { 'id': str(uuid.uuid4()) } created_db = self.client.CreateDatabase(database_definition) self.assertEqual(created_db['id'], database_definition['id']) # Read databases after creation. databases = list(self.client.ReadDatabases()) self.assertEqual(len(databases), before_create_databases_count + 1, 'create should increase the number of databases') # query databases. databases = list(self.client.QueryDatabases({ 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name':'@id', 'value': database_definition['id'] } ] })) self.assert_(databases, 'number of results for the query should be > 0') # read database. self.client.ReadDatabase(self.GetDatabaseLink(created_db, is_name_based)) # delete database. self.client.DeleteDatabase(self.GetDatabaseLink(created_db, is_name_based)) # read database after deletion self.__AssertHTTPFailureWithStatus(StatusCodes.NOT_FOUND, self.client.ReadDatabase, self.GetDatabaseLink(created_db, is_name_based)) def test_sql_query_crud(self): # create two databases. db1 = self.client.CreateDatabase({ 'id': 'database 1' }) db2 = self.client.CreateDatabase({ 'id': 'database 2' }) # query with parameters. databases = list(self.client.QueryDatabases({ 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name':'@id', 'value': 'database 1' } ] })) self.assertEqual(1, len(databases), 'Unexpected number of query results.') # query without parameters. databases = list(self.client.QueryDatabases({ 'query': 'SELECT * FROM root r WHERE r.id="database non-existing"' })) self.assertEqual(0, len(databases), 'Unexpected number of query results.') # query with a string. databases = list(self.client.QueryDatabases('SELECT * FROM root r WHERE r.id="database 2"')) self.assertEqual(1, len(databases), 'Unexpected number of query results.') self.client.DeleteDatabase(db1['_self']) self.client.DeleteDatabase(db2['_self']) def test_collection_crud_self_link(self): self._test_collection_crud(False) def test_collection_crud_name_based(self): self._test_collection_crud(True) def _test_collection_crud(self, is_name_based): created_db = self.databseForTest collections = list(self.client.ReadContainers(self.GetDatabaseLink(created_db, is_name_based))) # create a collection before_create_collections_count = len(collections) collection_definition = { 'id': 'test_collection_crud ' + str(uuid.uuid4()), 'indexingPolicy': {'indexingMode': 'consistent'} } created_collection = self.client.CreateContainer(self.GetDatabaseLink(created_db, is_name_based), collection_definition) self.assertEqual(collection_definition['id'], created_collection['id']) self.assertEqual('consistent', created_collection['indexingPolicy']['indexingMode']) # read collections after creation collections = list(self.client.ReadContainers(self.GetDatabaseLink(created_db, is_name_based))) self.assertEqual(len(collections), before_create_collections_count + 1, 'create should increase the number of collections') # query collections collections = list(self.client.QueryContainers( self.GetDatabaseLink(created_db, is_name_based), { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name':'@id', 'value': collection_definition['id'] } ] })) # Replacing indexing policy is allowed. lazy_policy = {'indexingMode': 'lazy'} created_collection['indexingPolicy'] = lazy_policy replaced_collection = self.client.ReplaceContainer(self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), created_collection) self.assertEqual('lazy', replaced_collection['indexingPolicy']['indexingMode']) # Replacing collection Id should fail. change_collection = created_collection.copy() change_collection['id'] = 'try_change_id' self.__AssertHTTPFailureWithStatus(StatusCodes.BAD_REQUEST, self.client.ReplaceContainer, self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), change_collection) self.assertTrue(collections) # delete collection self.client.DeleteContainer(self.GetDocumentCollectionLink(created_db, created_collection, is_name_based)) # read collection after deletion self.__AssertHTTPFailureWithStatus(StatusCodes.NOT_FOUND, self.client.ReadContainer, self.GetDocumentCollectionLink(created_db, created_collection, is_name_based)) def test_partitioned_collection(self): created_db = self.databseForTest collection_definition = { 'id': 'test_partitioned_collection ' + str(uuid.uuid4()), 'partitionKey': { 'paths': ['/id'], 'kind': documents.PartitionKind.Hash } } options = { 'offerThroughput': 10100 } created_collection = self.client.CreateContainer(self.GetDatabaseLink(created_db), collection_definition, options) self.assertEqual(collection_definition.get('id'), created_collection.get('id')) self.assertEqual(collection_definition.get('partitionKey').get('paths')[0], created_collection.get('partitionKey').get('paths')[0]) self.assertEqual(collection_definition.get('partitionKey').get('kind'), created_collection.get('partitionKey').get('kind')) offers = self.GetCollectionOffers(self.client, created_collection['_rid']) self.assertEqual(1, len(offers)) expected_offer = offers[0] self.assertEqual(expected_offer.get('content').get('offerThroughput'), options.get('offerThroughput')) self.client.DeleteContainer(self.GetDocumentCollectionLink(created_db, created_collection)) def test_partitioned_collection_quota(self): created_db = self.databseForTest options = { 'offerThroughput': 20000 } created_collection = self.configs.create_multi_partition_collection_if_not_exist(self.client) read_options = { 'populatePartitionKeyRangeStatistics': True, 'populateQuotaInfo': True} retrieved_collection = self.client.ReadContainer(created_collection.get('_self'), read_options) self.assertTrue(retrieved_collection.get("statistics") != None) self.assertTrue(self.client.last_response_headers.get("x-ms-resource-usage") != None) def test_partitioned_collection_partition_key_extraction(self): created_db = self.databseForTest collection_definition = { 'id': 'test_partitioned_collection_partition_key_extraction ' + str(uuid.uuid4()), 'partitionKey': { 'paths': ['/address/state'], 'kind': documents.PartitionKind.Hash } } created_collection = self.client.CreateContainer(self.GetDatabaseLink(created_db), collection_definition) document_definition = {'id': 'document1', 'address' : { 'street' : '1 Microsoft Way', 'city' : 'Redmond', 'state' : 'WA', 'zip code' : 98052 } } # create document without partition key being specified created_document = self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection), document_definition) self.assertEqual(created_document.get('id'), document_definition.get('id')) self.assertEqual(created_document.get('address').get('state'), document_definition.get('address').get('state')) # create document by specifying a different partition key in options than what's in the document will result in BadRequest(status code 400) document_definition['id'] = 'document2' options = { 'partitionKey': 'NY' } self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateItem, self.GetDocumentCollectionLink(created_db, created_collection), document_definition, options) collection_definition1 = { 'id': 'test_partitioned_collection_partition_key_extraction1 ' + str(uuid.uuid4()), 'partitionKey': { 'paths': ['/address'], 'kind': documents.PartitionKind.Hash } } created_collection1 = self.client.CreateContainer(self.GetDatabaseLink(created_db), collection_definition1) # Create document with partitionkey not present as a leaf level property but a dict options = {} created_document = self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection1), document_definition, options) self.assertEqual(options['partitionKey'], documents.Undefined) collection_definition2 = { 'id': 'test_partitioned_collection_partition_key_extraction2 ' + str(uuid.uuid4()), 'partitionKey': { 'paths': ['/address/state/city'], 'kind': documents.PartitionKind.Hash } } created_collection2 = self.client.CreateContainer(self.GetDatabaseLink(created_db), collection_definition2) # Create document with partitionkey not present in the document options = {} created_document = self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection2), document_definition, options) self.assertEqual(options['partitionKey'], documents.Undefined) self.client.DeleteContainer(self.GetDocumentCollectionLink(created_db, created_collection)) self.client.DeleteContainer(self.GetDocumentCollectionLink(created_db, created_collection1)) self.client.DeleteContainer(self.GetDocumentCollectionLink(created_db, created_collection2)) def test_partitioned_collection_partition_key_extraction_special_chars(self): created_db = self.databseForTest collection_definition1 = { 'id': 'test_partitioned_collection_partition_key_extraction_special_chars1 ' + str(uuid.uuid4()), 'partitionKey': { 'paths': ['/\"level\' 1*()\"/\"le/vel2\"'], 'kind': documents.PartitionKind.Hash } } created_collection1 = self.client.CreateContainer(self.GetDatabaseLink(created_db), collection_definition1) document_definition = {'id': 'document1', "level' 1*()" : { "le/vel2" : 'val1' } } options = {} self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection1), document_definition, options) self.assertEqual(options['partitionKey'], 'val1') collection_definition2 = { 'id': 'test_partitioned_collection_partition_key_extraction_special_chars2 ' + str(uuid.uuid4()), 'partitionKey': { 'paths': ['/\'level\" 1*()\'/\'le/vel2\''], 'kind': documents.PartitionKind.Hash } } created_collection2 = self.client.CreateContainer(self.GetDatabaseLink(created_db), collection_definition2) document_definition = {'id': 'document2', 'level\" 1*()' : { 'le/vel2' : 'val2' } } options = {} self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection2), document_definition, options) self.assertEqual(options['partitionKey'], 'val2') self.client.DeleteContainer(self.GetDocumentCollectionLink(created_db, created_collection1)) self.client.DeleteContainer(self.GetDocumentCollectionLink(created_db, created_collection2)) def test_partitioned_collection_path_parser(self): test_dir = os.path.dirname(os.path.abspath(__file__)) with open(os.path.join(test_dir, "BaselineTest.PathParser.json")) as json_file: entries = json.loads(json_file.read()) for entry in entries: parts = base.ParsePaths([entry['path']]) self.assertEqual(parts, entry['parts']) paths = ["/\"Ke \\ \\\" \\\' \\? \\a \\\b \\\f \\\n \\\r \\\t \\v y1\"/*"] parts = [ "Ke \\ \\\" \\\' \\? \\a \\\b \\\f \\\n \\\r \\\t \\v y1", "*" ] self.assertEqual(parts, base.ParsePaths(paths)) paths = ["/'Ke \\ \\\" \\\' \\? \\a \\\b \\\f \\\n \\\r \\\t \\v y1'/*"] parts = [ "Ke \\ \\\" \\\' \\? \\a \\\b \\\f \\\n \\\r \\\t \\v y1", "*" ] self.assertEqual(parts, base.ParsePaths(paths)) def test_partitioned_collection_document_crud_and_query(self): created_db = self.databseForTest created_collection = self.configs.create_multi_partition_collection_if_not_exist(self.client) document_definition = {'id': 'document', 'key': 'value'} created_document = self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection), document_definition) self.assertEqual(created_document.get('id'), document_definition.get('id')) self.assertEqual(created_document.get('key'), document_definition.get('key')) # For ReadDocument, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) #self.__AssertHTTPFailureWithStatus( # StatusCodes.BAD_REQUEST, # client.ReadItem, # self.GetDocumentLink(created_db, created_collection, created_document)) # read document options = { 'partitionKey': document_definition.get('id') } read_document = self.client.ReadItem( self.GetDocumentLink(created_db, created_collection, created_document), options) self.assertEqual(read_document.get('id'), created_document.get('id')) self.assertEqual(read_document.get('key'), created_document.get('key')) # Read document feed doesn't require partitionKey as it's always a cross partition query documentlist = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, created_collection))) self.assertEqual(1, len(documentlist)) # replace document document_definition['key'] = 'new value' replaced_document = self.client.ReplaceItem( self.GetDocumentLink(created_db, created_collection, created_document), document_definition) self.assertEqual(replaced_document.get('key'), document_definition.get('key')) # upsert document(create scenario) document_definition['id'] = 'document2' document_definition['key'] = 'value2' upserted_document = self.client.UpsertItem(self.GetDocumentCollectionLink(created_db, created_collection), document_definition) self.assertEqual(upserted_document.get('id'), document_definition.get('id')) self.assertEqual(upserted_document.get('key'), document_definition.get('key')) documentlist = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, created_collection))) self.assertEqual(2, len(documentlist)) # For DeleteDocument, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.DeleteItem, self.GetDocumentLink(created_db, created_collection, upserted_document)) # delete document options = { 'partitionKey': upserted_document.get('id') } self.client.DeleteItem( self.GetDocumentLink(created_db, created_collection, upserted_document), options) # query document on the partition key specified in the predicate will pass even without setting enableCrossPartitionQuery or passing in the partitionKey value documentlist = list(self.client.QueryItems( self.GetDocumentCollectionLink(created_db, created_collection), { 'query': 'SELECT * FROM root r WHERE r.id=\'' + replaced_document.get('id') + '\'' })) self.assertEqual(1, len(documentlist)) # query document on any property other than partitionKey will fail without setting enableCrossPartitionQuery or passing in the partitionKey value try: list(self.client.QueryItems( self.GetDocumentCollectionLink(created_db, created_collection), { 'query': 'SELECT * FROM root r WHERE r.key=\'' + replaced_document.get('key') + '\'' })) except Exception: pass # cross partition query options = { 'enableCrossPartitionQuery': True } documentlist = list(self.client.QueryItems( self.GetDocumentCollectionLink(created_db, created_collection), { 'query': 'SELECT * FROM root r WHERE r.key=\'' + replaced_document.get('key') + '\'' }, options)) self.assertEqual(1, len(documentlist)) # query document by providing the partitionKey value options = { 'partitionKey': replaced_document.get('id') } documentlist = list(self.client.QueryItems( self.GetDocumentCollectionLink(created_db, created_collection), { 'query': 'SELECT * FROM root r WHERE r.key=\'' + replaced_document.get('key') + '\'' }, options)) self.assertEqual(1, len(documentlist)) def test_partitioned_collection_permissions(self): created_db = self.databseForTest collection_definition = { 'id': 'sample collection ' + str(uuid.uuid4()), 'partitionKey': { 'paths': ['/key'], 'kind': documents.PartitionKind.Hash } } collection_definition['id'] = 'test_partitioned_collection_permissions all collection' all_collection = self.client.CreateContainer(self.GetDatabaseLink(created_db), collection_definition) collection_definition['id'] = 'test_partitioned_collection_permissions read collection' read_collection = self.client.CreateContainer(self.GetDatabaseLink(created_db), collection_definition) user = self.client.CreateUser(self.GetDatabaseLink(created_db), { 'id': 'user' }) permission_definition = { 'id': 'all permission', 'permissionMode': documents.PermissionMode.All, 'resource': self.GetDocumentCollectionLink(created_db, all_collection), 'resourcePartitionKey' : [1] } all_permission = self.client.CreatePermission(self.GetUserLink(created_db, user), permission_definition) permission_definition = { 'id': 'read permission', 'permissionMode': documents.PermissionMode.Read, 'resource': self.GetDocumentCollectionLink(created_db, read_collection), 'resourcePartitionKey' : [1] } read_permission = self.client.CreatePermission(self.GetUserLink(created_db, user), permission_definition) resource_tokens = {} # storing the resource tokens based on Resource IDs resource_tokens[all_collection['_rid']] = (all_permission['_token']) resource_tokens[read_collection['_rid']] = (read_permission['_token']) restricted_client = cosmos_client.CosmosClient( CRUDTests.host, {'resourceTokens': resource_tokens}, CRUDTests.connectionPolicy) document_definition = {'id': 'document1', 'key': 1 } # Create document in all_collection should succeed since the partitionKey is 1 which is what specified as resourcePartitionKey in permission object and it has all permissions created_document = restricted_client.CreateItem( self.GetDocumentCollectionLink(created_db, all_collection, False), document_definition) # Create document in read_collection should fail since it has only read permissions for this collection self.__AssertHTTPFailureWithStatus( StatusCodes.FORBIDDEN, restricted_client.CreateItem, self.GetDocumentCollectionLink(created_db, read_collection, False), document_definition) # Read document feed should succeed for this collection. Note that I need to pass in partitionKey here since permission has resourcePartitionKey defined options = { 'partitionKey': document_definition.get('key') } documentlist = list(restricted_client.ReadItems( self.GetDocumentCollectionLink(created_db, read_collection, False), options)) self.assertEqual(0, len(documentlist)) document_definition['key'] = 2 options = { 'partitionKey': document_definition.get('key') } # Create document should fail since the partitionKey is 2 which is different that what is specified as resourcePartitionKey in permission object self.__AssertHTTPFailureWithStatus( StatusCodes.FORBIDDEN, restricted_client.CreateItem, self.GetDocumentCollectionLink(created_db, all_collection, False), document_definition, options) document_definition['key'] = 1 options = { 'partitionKey': document_definition.get('key') } # Delete document should succeed since the partitionKey is 1 which is what specified as resourcePartitionKey in permission object created_document = restricted_client.DeleteItem( self.GetDocumentLink(created_db, all_collection, created_document, False), options) # Delete document in read_collection should fail since it has only read permissions for this collection self.__AssertHTTPFailureWithStatus( StatusCodes.FORBIDDEN, restricted_client.DeleteItem, self.GetDocumentCollectionLink(created_db, read_collection, False), options) self.client.DeleteContainer(self.GetDocumentCollectionLink(created_db, all_collection)) self.client.DeleteContainer(self.GetDocumentCollectionLink(created_db, read_collection)) def test_partitioned_collection_execute_stored_procedure(self): created_db = self.databseForTest created_collection = self.configs.create_multi_partition_collection_with_custom_pk_if_not_exist(self.client) sproc = { 'id': 'storedProcedure' + str(uuid.uuid4()), 'body': ( 'function () {' + ' var client = getContext().getCollection();' + ' client.createDocument(client.getSelfLink(), { id: \'testDoc\', pk : 2}, {}, function(err, docCreated, options) { ' + ' if(err) throw new Error(\'Error while creating document: \' + err.message);' + ' else {' + ' getContext().getResponse().setBody(1);' + ' }' + ' });}') } created_sproc = self.client.CreateStoredProcedure(self.GetDocumentCollectionLink(created_db, created_collection), sproc) # Partiton Key value same as what is specified in the stored procedure body self.client.ExecuteStoredProcedure(self.GetStoredProcedureLink(created_db, created_collection, created_sproc), None, { 'partitionKey' : 2}) # Partiton Key value different than what is specified in the stored procedure body will cause a bad request(400) error self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.ExecuteStoredProcedure, self.GetStoredProcedureLink(created_db, created_collection, created_sproc), None, { 'partitionKey' : 3}) def test_partitioned_collection_attachment_crud_and_query(self): class ReadableStream(object): """Customized file-like stream. """ def __init__(self, chunks = ['first chunk ', 'second chunk']): """Initialization. :Parameters: - `chunks`: list """ if six.PY2: self._chunks = list(chunks) else: # python3: convert to bytes self._chunks = [chunk.encode() for chunk in chunks] def read(self, n=-1): """Simulates the read method in a file stream. :Parameters: - `n`: int :Returns: bytes or str """ if self._chunks: return self._chunks.pop(0) else: return '' def __len__(self): """To make len(ReadableStream) work. """ return sum([len(chunk) for chunk in self._chunks]) db = self.databseForTest collection_definition = {'id': 'test_partitioned_collection_attachment_crud_and_query ' + str(uuid.uuid4()), 'partitionKey': {'paths': ['/id'],'kind': 'Hash'}} collection = self.client.CreateContainer(db['_self'], collection_definition) document_definition = {'id': 'sample document' + str(uuid.uuid4()), 'key': 'value'} document = self.client.CreateItem(self.GetDocumentCollectionLink(db, collection), document_definition) content_stream = ReadableStream() options = { 'slug': 'sample attachment', 'contentType': 'application/text' } # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) #self.__AssertHTTPFailureWithStatus( # StatusCodes.BAD_REQUEST, # client.CreateAttachmentAndUploadMedia, # self.GetDocumentLink(db, collection, document), # content_stream, # options) content_stream = ReadableStream() # Setting the partitionKey as part of options is required for attachment CRUD options = { 'slug': 'sample attachment' + str(uuid.uuid4()), 'contentType': 'application/text', 'partitionKey' : document_definition.get('id') } # create attachment and upload media attachment = self.client.CreateAttachmentAndUploadMedia( self.GetDocumentLink(db, collection, document), content_stream, options) self.assertEqual(attachment['id'], options['slug']) # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) try: list(self.client.ReadAttachments( self.GetDocumentLink(db, collection, document))) except Exception: pass # Read attachment feed requires partitionKey to be passed options = { 'partitionKey': document_definition.get('id') } attachmentlist = list(self.client.ReadAttachments( self.GetDocumentLink(db, collection, document), options)) self.assertEqual(1, len(attachmentlist)) content_stream = ReadableStream() options = { 'slug': 'new attachment' + str(uuid.uuid4()), 'contentType': 'application/text' } # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.UpsertAttachmentAndUploadMedia, self.GetDocumentLink(db, collection, document), content_stream, options) content_stream = ReadableStream() # Setting the partitionKey as part of options is required for attachment CRUD options = { 'slug': 'new attachment' + str(uuid.uuid4()), 'contentType': 'application/text', 'partitionKey' : document_definition.get('id') } # upsert attachment and upload media attachment = self.client.UpsertAttachmentAndUploadMedia( self.GetDocumentLink(db, collection, document), content_stream, options) self.assertEqual(attachment['id'], options['slug']) options = { 'partitionKey': document_definition.get('id') } attachmentlist = list(self.client.ReadAttachments( self.GetDocumentLink(db, collection, document), options)) self.assertEqual(2, len(attachmentlist)) # create attachment with media link dynamic_attachment = { 'id': 'dynamic attachment' + str(uuid.uuid4()), 'media': 'http://xstore.', 'MediaType': 'Book', 'Author':'My Book Author', 'Title':'My Book Title', 'contentType':'application/text' } # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateAttachment, self.GetDocumentLink(db, collection, document), dynamic_attachment) # create dynamic attachment options = { 'partitionKey': document_definition.get('id') } attachment = self.client.CreateAttachment(self.GetDocumentLink(db, collection, document), dynamic_attachment, options) self.assertEqual(attachment['MediaType'], dynamic_attachment['MediaType']) self.assertEqual(attachment['Author'], dynamic_attachment['Author']) # Read Attachment feed options = { 'partitionKey': document_definition.get('id') } attachmentlist = list(self.client.ReadAttachments( self.GetDocumentLink(db, collection, document), options)) self.assertEqual(3, len(attachmentlist)) # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) #self.__AssertHTTPFailureWithStatus( # StatusCodes.BAD_REQUEST, # client.ReadAttachment, # self.GetAttachmentLink(db, collection, document, attachment)) # Read attachment options = { 'partitionKey': document_definition.get('id') } read_attachment = self.client.ReadAttachment(self.GetAttachmentLink(db, collection, document, attachment), options) self.assertEqual(attachment['id'], read_attachment['id']) attachment['Author'] = 'new author' # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.ReplaceAttachment, self.GetAttachmentLink(db, collection, document, attachment), attachment) # replace the attachment options = { 'partitionKey': document_definition.get('id') } replaced_attachment = self.client.ReplaceAttachment(self.GetAttachmentLink(db, collection, document, attachment), attachment, options) self.assertEqual(attachment['id'], replaced_attachment['id']) self.assertEqual(attachment['Author'], replaced_attachment['Author']) attachment['id'] = 'new dynamic attachment' + str(uuid.uuid4()) attachment['Title'] = 'new title' # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.UpsertAttachment, self.GetDocumentLink(db, collection, document), attachment) # upsert attachment(create scenario) options = { 'partitionKey': document_definition.get('id') } upserted_attachment = self.client.UpsertAttachment(self.GetDocumentLink(db, collection, document), attachment, options) self.assertEqual(attachment['id'], upserted_attachment['id']) self.assertEqual(attachment['Title'], upserted_attachment['Title']) # query attachments will fail without passing in the partitionKey value try: list(self.client.QueryAttachments( self.GetDocumentLink(db, collection, document), { 'query': 'SELECT * FROM root r WHERE r.MediaType=\'' + dynamic_attachment.get('MediaType') + '\'' })) except Exception: pass # query attachments by providing the partitionKey value options = { 'partitionKey': document_definition.get('id') } attachmentlist = list(self.client.QueryAttachments( self.GetDocumentLink(db, collection, document), { 'query': 'SELECT * FROM root r WHERE r.MediaType=\'' + dynamic_attachment.get('MediaType') + '\'' }, options)) self.assertEqual(2, len(attachmentlist)) # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.DeleteAttachment, self.GetAttachmentLink(db, collection, document, attachment)) # deleting attachment options = { 'partitionKey': document_definition.get('id') } self.client.DeleteAttachment(self.GetAttachmentLink(db, collection, document, attachment), options) self.client.DeleteContainer(collection['_self']) def test_partitioned_collection_partition_key_value_types(self): created_db = self.databseForTest created_collection = self.configs.create_multi_partition_collection_with_custom_pk_if_not_exist(self.client) document_definition = {'id': 'document1' + str(uuid.uuid4()), 'pk' : None, 'spam': 'eggs'} # create document with partitionKey set as None here self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection), document_definition) document_definition = {'id': 'document1' + str(uuid.uuid4()), 'spam': 'eggs'} # create document with partitionKey set as Undefined here self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection), document_definition) document_definition = {'id': 'document1' + str(uuid.uuid4()), 'pk' : True, 'spam': 'eggs'} # create document with bool partitionKey self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection), document_definition) document_definition = {'id': 'document1' + str(uuid.uuid4()), 'pk' : 'value', 'spam': 'eggs'} # create document with string partitionKey self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection), document_definition) document_definition = {'id': 'document1' + str(uuid.uuid4()), 'pk' : 100, 'spam': 'eggs'} # create document with int partitionKey self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection), document_definition) document_definition = {'id': 'document1' + str(uuid.uuid4()), 'pk' : 10.50, 'spam': 'eggs'} # create document with float partitionKey self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection), document_definition) def test_partitioned_collection_conflict_crud_and_query(self): created_db = self.databseForTest created_collection = self.configs.create_multi_partition_collection_if_not_exist(self.client) conflict_definition = {'id': 'new conflict', 'resourceId' : 'doc1', 'operationType' : 'create', 'resourceType' : 'document' } # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) #self.__AssertHTTPFailureWithStatus( # StatusCodes.BAD_REQUEST, # client.ReadConflict, # self.GetConflictLink(created_db, created_collection, conflict_definition)) # read conflict here will return resource not found(404) since there is no conflict here options = { 'partitionKey': conflict_definition.get('id') } self.__AssertHTTPFailureWithStatus( StatusCodes.NOT_FOUND, self.client.ReadConflict, self.GetConflictLink(created_db, created_collection, conflict_definition), options) # Read conflict feed doesn't requires partitionKey to be specified as it's a cross partition thing conflictlist = list(self.client.ReadConflicts(self.GetDocumentCollectionLink(created_db, created_collection))) self.assertEqual(0, len(conflictlist)) # Currently, we require to have the partitionKey to be specified as part of options otherwise we get BadRequest(status code 400) self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.DeleteConflict, self.GetConflictLink(created_db, created_collection, conflict_definition)) # delete conflict here will return resource not found(404) since there is no conflict here options = { 'partitionKey': conflict_definition.get('id') } self.__AssertHTTPFailureWithStatus( StatusCodes.NOT_FOUND, self.client.DeleteConflict, self.GetConflictLink(created_db, created_collection, conflict_definition), options) # query conflicts on any property other than partitionKey will fail without setting enableCrossPartitionQuery or passing in the partitionKey value try: list(self.client.QueryConflicts( self.GetDocumentCollectionLink(created_db, created_collection), { 'query': 'SELECT * FROM root r WHERE r.resourceType=\'' + conflict_definition.get('resourceType') + '\'' })) except Exception: pass # cross partition query options = { 'enableCrossPartitionQuery': True } conflictlist = list(self.client.QueryConflicts( self.GetDocumentCollectionLink(created_db, created_collection), { 'query': 'SELECT * FROM root r WHERE r.resourceType=\'' + conflict_definition.get('resourceType') + '\'' }, options)) self.assertEqual(0, len(conflictlist)) # query conflicts by providing the partitionKey value options = { 'partitionKey': conflict_definition.get('id') } conflictlist = list(self.client.QueryConflicts( self.GetDocumentCollectionLink(created_db, created_collection), { 'query': 'SELECT * FROM root r WHERE r.resourceType=\'' + conflict_definition.get('resourceType') + '\'' }, options)) self.assertEqual(0, len(conflictlist)) def test_document_crud_self_link(self): self._test_document_crud(False) def test_document_crud_name_based(self): self._test_document_crud(True) def _test_document_crud(self, is_name_based): # create database created_db = self.databseForTest # create collection created_collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # read documents documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based))) # create a document before_create_documents_count = len(documents) document_definition = {'name': 'sample document', 'spam': 'eggs', 'key': 'value'} # Should throw an error because automatic id generation is disabled. self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateItem, self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), document_definition, {'disableAutomaticIdGeneration': True}) created_document = self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), document_definition) self.assertEqual(created_document['name'], document_definition['name']) self.assertTrue(created_document['id'] != None) # duplicated documents are allowed when 'id' is not provided. duplicated_document = self.client.CreateItem( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), document_definition) self.assertEqual(duplicated_document['name'], document_definition['name']) self.assert_(duplicated_document['id']) self.assertNotEqual(duplicated_document['id'], created_document['id']) # duplicated documents are not allowed when 'id' is provided. duplicated_definition_with_id = document_definition.copy() duplicated_definition_with_id['id'] = created_document['id'] self.__AssertHTTPFailureWithStatus(StatusCodes.CONFLICT, self.client.CreateItem, self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), duplicated_definition_with_id) # read documents after creation documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based))) self.assertEqual( len(documents), before_create_documents_count + 2, 'create should increase the number of documents') # query documents documents = list(self.client.QueryItems( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), { 'query': 'SELECT * FROM root r WHERE r.name=@name', 'parameters': [ { 'name':'@name', 'value':document_definition['name'] } ] })) self.assert_(documents) documents = list(self.client.QueryItems( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), { 'query': 'SELECT * FROM root r WHERE r.name=@name', 'parameters': [ { 'name':'@name', 'value':document_definition['name'] } ] }, { 'enableScanInQuery': True})) self.assert_(documents) # replace document. created_document['name'] = 'replaced document' created_document['spam'] = 'not eggs' old_etag = created_document['_etag'] replaced_document = self.client.ReplaceItem( self.GetDocumentLink(created_db, created_collection, created_document, is_name_based), created_document) self.assertEqual(replaced_document['name'], 'replaced document', 'document id property should change') self.assertEqual(replaced_document['spam'], 'not eggs', 'property should have changed') self.assertEqual(created_document['id'], replaced_document['id'], 'document id should stay the same') #replace document based on condition replaced_document['name'] = 'replaced document based on condition' replaced_document['spam'] = 'new spam field' #should fail for stale etag self.__AssertHTTPFailureWithStatus( StatusCodes.PRECONDITION_FAILED, self.client.ReplaceItem, self.GetDocumentLink(created_db, created_collection, replaced_document, is_name_based), replaced_document, { 'accessCondition' : { 'type': 'IfMatch', 'condition': old_etag } }) #should pass for most recent etag replaced_document_conditional = self.client.ReplaceItem( self.GetDocumentLink(created_db, created_collection, replaced_document, is_name_based), replaced_document, { 'accessCondition' : { 'type': 'IfMatch', 'condition': replaced_document['_etag'] } }) self.assertEqual(replaced_document_conditional['name'], 'replaced document based on condition', 'document id property should change') self.assertEqual(replaced_document_conditional['spam'], 'new spam field', 'property should have changed') self.assertEqual(replaced_document_conditional['id'], replaced_document['id'], 'document id should stay the same') # read document one_document_from_read = self.client.ReadItem( self.GetDocumentLink(created_db, created_collection, replaced_document, is_name_based)) self.assertEqual(replaced_document['id'], one_document_from_read['id']) # delete document self.client.DeleteItem(self.GetDocumentLink(created_db, created_collection, replaced_document, is_name_based)) # read documents after deletion self.__AssertHTTPFailureWithStatus(StatusCodes.NOT_FOUND, self.client.ReadItem, self.GetDocumentLink(created_db, created_collection, replaced_document, is_name_based)) def test_partitioning(self): # create test database created_db = self.databseForTest # Create bunch of collections participating in partitioning collection0 = self.client.CreateContainer( self.GetDatabaseLink(created_db, True), { 'id': 'test_partitioning coll_0' + str(uuid.uuid4()) }) collection1 = self.client.CreateContainer( self.GetDatabaseLink(created_db, True), { 'id': 'test_partitioning coll_1' + str(uuid.uuid4())}) collection2 = self.client.CreateContainer( self.GetDatabaseLink(created_db, True), { 'id': 'test_partitioning coll_2' + str(uuid.uuid4())}) # Register the collection links for partitioning through partition resolver collection_links = [self.GetDocumentCollectionLink(created_db, collection0, True), self.GetDocumentCollectionLink(created_db, collection1, True), self.GetDocumentCollectionLink(created_db, collection2, True)] partition_resolver = test_partition_resolver.TestPartitionResolver(collection_links) self.client.RegisterPartitionResolver(self.GetDatabaseLink(created_db, True), partition_resolver) # create a document using the document definition document_definition = { 'id': '0', 'name': 'sample document', 'key': 'value' } self.client.CreateItem( self.GetDatabaseLink(created_db, True), document_definition) # Read the documents in collection1 and verify that the count is 1 now documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, collection0, True))) self.assertEqual(1, len(documents)) # Verify that it contains the document with Id 0 self.assertEqual('0', documents[0]['id']) document_definition['id'] = '1' self.client.CreateItem( self.GetDatabaseLink(created_db, True), document_definition) # Read the documents in collection1 and verify that the count is 1 now documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, collection1, True))) self.assertEqual(1, len(documents)) # Verify that it contains the document with Id 1 self.assertEqual('1', documents[0]['id']) document_definition['id'] = '2' self.client.CreateItem( self.GetDatabaseLink(created_db, True), document_definition) # Read the documents in collection2 and verify that the count is 1 now documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, collection2, True))) self.assertEqual(1, len(documents)) # Verify that it contains the document with Id 2 self.assertEqual('2', documents[0]['id']) # Updating the value of "key" property to test UpsertDocument(replace scenario) document_definition['id'] = '0' document_definition['key'] = 'new value' self.client.UpsertItem( self.GetDatabaseLink(created_db, True), document_definition) # Read the documents in collection0 and verify that the count is still 1 documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, collection0, True))) self.assertEqual(1, len(documents)) # Verify that it contains the document with new key value self.assertEqual(document_definition['key'], documents[0]['key']) # Query documents in all collections(since no partition key specified) using query string documents = list(self.client.QueryItems( self.GetDatabaseLink(created_db, True), { 'query': 'SELECT * FROM root r WHERE r.id=\'2\'' })) self.assertEqual(1, len(documents)) # Updating the value of id property to test UpsertDocument(create scenario) document_definition['id'] = '4' self.client.UpsertItem( self.GetDatabaseLink(created_db, True), document_definition) # Read the documents in collection1 and verify that the count is 2 now documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, collection1, True))) self.assertEqual(2, len(documents)) # Query documents in all collections(since no partition key specified) using query spec documents = list(self.client.QueryItems( self.GetDatabaseLink(created_db, True), { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name':'@id', 'value':document_definition['id'] } ] })) self.assertEqual(1, len(documents)) # Query documents in collection(with partition key of '4' specified) which resolves to collection1 documents = list(self.client.QueryItems( self.GetDatabaseLink(created_db, True), { 'query': 'SELECT * FROM root r' }, {}, document_definition['id'])) self.assertEqual(2, len(documents)) # Query documents in collection(with partition key '5' specified) which resolves to collection2 but non existent document in that collection documents = list(self.client.QueryItems( self.GetDatabaseLink(created_db, True), { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name':'@id', 'value':document_definition['id'] } ] }, {}, '5')) self.assertEqual(0, len(documents)) self.client.DeleteContainer(collection0['_self']) self.client.DeleteContainer(collection1['_self']) self.client.DeleteContainer(collection2['_self']) # Partitioning test(with paging) def test_partition_paging(self): # create test database created_db = self.databseForTest # Create bunch of collections participating in partitioning collection0 = self.client.CreateContainer( self.GetDatabaseLink(created_db, True), { 'id': 'test_partition_paging coll_0 ' + str(uuid.uuid4()) }) collection1 = self.client.CreateContainer( self.GetDatabaseLink(created_db, True), { 'id': 'test_partition_paging coll_1 ' + str(uuid.uuid4()) }) # Register the collection links for partitioning through partition resolver collection_links = [self.GetDocumentCollectionLink(created_db, collection0, True), self.GetDocumentCollectionLink(created_db, collection1, True)] partition_resolver = test_partition_resolver.TestPartitionResolver(collection_links) self.client.RegisterPartitionResolver(self.GetDatabaseLink(created_db, True), partition_resolver) # Create document definition used to create documents document_definition = { 'id': '0', 'name': 'sample document', 'key': 'value' } # Create 10 documents each with a different id starting from 0 to 9 for i in xrange(0, 10): document_definition['id'] = str(i) self.client.CreateItem( self.GetDatabaseLink(created_db, True), document_definition) # Query the documents to ensure that you get the correct count(no paging) documents = list(self.client.QueryItems( self.GetDatabaseLink(created_db, True), { 'query': 'SELECT * FROM root r WHERE r.id < \'7\'' })) self.assertEqual(7, len(documents)) # Query the documents with maxItemCount to restrict the max number of documents returned queryIterable = self.client.QueryItems( self.GetDatabaseLink(created_db, True), { 'query': 'SELECT * FROM root r WHERE r.id < \'7\'' }, {'maxItemCount':3}) # Query again and count the number of documents(with paging) docCount = 0 for _ in queryIterable: docCount += 1 self.assertEqual(7, docCount) # Query again to test fetch_next_block to ensure that it returns the correct number of documents everytime it's called queryIterable = self.client.QueryItems( self.GetDatabaseLink(created_db, True), { 'query': 'SELECT * FROM root r WHERE r.id < \'7\'' }, {'maxItemCount':3}) # Documents with id 0, 2, 4(in collection0) self.assertEqual(3, len(queryIterable.fetch_next_block())) # Documents with id 6(in collection0) self.assertEqual(1, len(queryIterable.fetch_next_block())) # Documents with id 1, 3, 5(in collection1) self.assertEqual(3, len(queryIterable.fetch_next_block())) # No more documents self.assertEqual(0, len(queryIterable.fetch_next_block())) # Set maxItemCount to -1 to lift the limit on max documents returned by the query queryIterable = self.client.QueryItems( self.GetDatabaseLink(created_db, True), { 'query': 'SELECT * FROM root r WHERE r.id < \'7\'' }, {'maxItemCount':-1}) # Documents with id 0, 2, 4, 6(all docs in collection0 adhering to query condition) self.assertEqual(4, len(queryIterable.fetch_next_block())) # Documents with id 1, 3, 5(all docs in collection1 adhering to query condition) self.assertEqual(3, len(queryIterable.fetch_next_block())) # No more documents self.assertEqual(0, len(queryIterable.fetch_next_block())) self.client.DeleteContainer(collection0['_self']) self.client.DeleteContainer(collection1['_self']) def test_hash_partition_resolver(self): created_db = self.databseForTest # Create bunch of collections participating in partitioning collection0 = { 'id': 'coll_0 ' + str(uuid.uuid4()) } collection1 = { 'id': 'coll_1 ' + str(uuid.uuid4()) } collection_links = [self.GetDocumentCollectionLink(created_db, collection0, True), self.GetDocumentCollectionLink(created_db, collection1, True)] id_partition_key_extractor = lambda document: document['id'] hashpartition_resolver = hash_partition_resolver.HashPartitionResolver(id_partition_key_extractor, collection_links) # create a document using the document definition document_definition = { 'id': '0', 'name': 'sample document', 'key': 'value' } document_definition['id'] = '2' collection_link = hashpartition_resolver.ResolveForCreate(document_definition) read_collection_links = hashpartition_resolver.ResolveForRead(document_definition['id']) self.assertEqual(1, len(read_collection_links)) self.assertEqual(collection_link, read_collection_links[0]) def test_consistent_hash_ring(self): created_db = { 'id': 'db' } collection_links = list() expected_partition_list = list() total_collections_count = 2 collection = { 'id': 'coll' } for i in xrange(0, total_collections_count): collection['id'] = 'coll' + str(i) collection_link = self.GetDocumentCollectionLink(created_db, collection, True) collection_links.append(collection_link) expected_partition_list.append(('dbs/db/colls/coll0', 1076200484)) expected_partition_list.append(('dbs/db/colls/coll0', 1302652881)) expected_partition_list.append(('dbs/db/colls/coll0', 2210251988)) expected_partition_list.append(('dbs/db/colls/coll1', 2341558382)) expected_partition_list.append(('dbs/db/colls/coll0', 2348251587)) expected_partition_list.append(('dbs/db/colls/coll0', 2887945459)) expected_partition_list.append(('dbs/db/colls/coll1', 2894403633)) expected_partition_list.append(('dbs/db/colls/coll1', 3031617259)) expected_partition_list.append(('dbs/db/colls/coll1', 3090861424)) expected_partition_list.append(('dbs/db/colls/coll1', 4222475028)) id_partition_key_extractor = lambda document: document['id'] hashpartition_resolver = hash_partition_resolver.HashPartitionResolver(id_partition_key_extractor, collection_links, 5) actual_partition_list = hashpartition_resolver.consistent_hash_ring._GetSerializedPartitionList() self.assertEqual(len(expected_partition_list), len(actual_partition_list)) for i in xrange(0, len(expected_partition_list)): self.assertEqual(actual_partition_list[i][0], expected_partition_list[i][0]) self.assertEqual(actual_partition_list[i][1], expected_partition_list[i][1]) # Querying for a document and verifying that it's in the expected collection read_collection_links = hashpartition_resolver.ResolveForRead("beadledom") self.assertEqual(1, len(read_collection_links)) collection['id'] = 'coll1' collection_link = self.GetDocumentCollectionLink(created_db, collection, True) self.assertTrue(collection_link in read_collection_links) # Querying for a document and verifying that it's in the expected collection read_collection_links = hashpartition_resolver.ResolveForRead("999") self.assertEqual(1, len(read_collection_links)) collection['id'] = 'coll0' collection_link = self.GetDocumentCollectionLink(created_db, collection, True) self.assertTrue(collection_link in read_collection_links) def test_murmur_hash(self): str = 'afdgdd' bytes = bytearray(str, encoding='utf-8') hash_value = murmur_hash._MurmurHash._ComputeHash(bytes) self.assertEqual(1099701186, hash_value) num = 374.0 bytes = bytearray(pack('d', num)) hash_value = murmur_hash._MurmurHash._ComputeHash(bytes) self.assertEqual(3717946798, hash_value) self._validate_bytes("", 0x1B873593, bytearray(b'\xEE\xA8\xA2\x67'), 1738713326) self._validate_bytes("1", 0xE82562E4, bytearray(b'\xD0\x92\x24\xED'), 3978597072) self._validate_bytes("00", 0xB4C39035, bytearray(b'\xFA\x09\x64\x1B'), 459540986) self._validate_bytes("eyetooth", 0x8161BD86, bytearray(b'\x98\x62\x1C\x6F'), 1864131224) self._validate_bytes("acid", 0x4DFFEAD7, bytearray(b'\x36\x92\xC0\xB9'), 3116405302) self._validate_bytes("elevation", 0x1A9E1828, bytearray(b'\xA9\xB6\x40\xDF'), 3745560233) self._validate_bytes("dent", 0xE73C4579, bytearray(b'\xD4\x59\xE1\xD3'), 3554761172) self._validate_bytes("homeland", 0xB3DA72CA, bytearray(b'\x06\x4D\x72\xBB'), 3144830214) self._validate_bytes("glamor", 0x8078A01B, bytearray(b'\x89\x89\xA2\xA7'), 2812447113) self._validate_bytes("flags", 0x4D16CD6C, bytearray(b'\x52\x87\x66\x02'), 40273746) self._validate_bytes("democracy", 0x19B4FABD, bytearray(b'\xE4\x55\xD6\xB0'), 2966836708) self._validate_bytes("bumble", 0xE653280E, bytearray(b'\xFE\xD7\xC3\x0C'), 214161406) self._validate_bytes("catch", 0xB2F1555F, bytearray(b'\x98\x4B\xB6\xCD'), 3451276184) self._validate_bytes("omnomnomnivore", 0x7F8F82B0, bytearray(b'\x38\xC4\xCD\xFF'), 4291675192) self._validate_bytes("The quick brown fox jumps over the lazy dog", 0x4C2DB001, bytearray(b'\x6D\xAB\x8D\xC9'), 3381504877) def _validate_bytes(self, str, seed, expected_hash_bytes, expected_value): hash_value = murmur_hash._MurmurHash._ComputeHash(bytearray(str, encoding='utf-8'), seed) bytes = bytearray(pack('I', hash_value)) self.assertEqual(expected_value, hash_value) self.assertEqual(expected_hash_bytes, bytes) def test_get_bytes(self): actual_bytes = consistent_hash_ring._ConsistentHashRing._GetBytes("documentdb") expected_bytes = bytearray(b'\x64\x6F\x63\x75\x6D\x65\x6E\x74\x64\x62') self.assertEqual(expected_bytes, actual_bytes) actual_bytes = consistent_hash_ring._ConsistentHashRing._GetBytes("azure") expected_bytes = bytearray(b'\x61\x7A\x75\x72\x65') self.assertEqual(expected_bytes, actual_bytes) actual_bytes = consistent_hash_ring._ConsistentHashRing._GetBytes("json") expected_bytes = bytearray(b'\x6A\x73\x6F\x6E') self.assertEqual(expected_bytes, actual_bytes) actual_bytes = consistent_hash_ring._ConsistentHashRing._GetBytes("nosql") expected_bytes = bytearray(b'\x6E\x6F\x73\x71\x6C') self.assertEqual(expected_bytes, actual_bytes) def test_range_partition_resolver(self): # create test database created_db = self.databseForTest # Create bunch of collections participating in partitioning collection0 = { 'id': 'coll_0' } collection1 = { 'id': 'coll_1' } collection2 = { 'id': 'coll_2' } collection_links = [self.GetDocumentCollectionLink(created_db, collection0, True), self.GetDocumentCollectionLink(created_db, collection1, True), self.GetDocumentCollectionLink(created_db, collection2, True)] val_partition_key_extractor = lambda document: document['val'] ranges =[partition_range.Range(0,400), partition_range.Range(401,800), partition_range.Range(501,1200)] partition_map = dict([(ranges[0],collection_links[0]), (ranges[1],collection_links[1]), (ranges[2],collection_links[2])]) rangepartition_resolver = range_partition_resolver.RangePartitionResolver(val_partition_key_extractor, partition_map) # create a document using the document definition document_definition = { 'id': '0', 'name': 'sample document', 'val': 0 } document_definition['val'] = 400 collection_link = rangepartition_resolver.ResolveForCreate(document_definition) self.assertEquals(collection_links[0], collection_link) read_collection_links = rangepartition_resolver.ResolveForRead(600) self.assertEqual(2, len(read_collection_links)) self.assertTrue(collection_links[1] in read_collection_links) self.assertTrue(collection_links[2] in read_collection_links) read_collection_links = rangepartition_resolver.ResolveForRead(partition_range.Range(250, 500)) self.assertEqual(2, len(read_collection_links)) self.assertTrue(collection_links[0] in read_collection_links) self.assertTrue(collection_links[1] in read_collection_links) read_collection_links = rangepartition_resolver.ResolveForRead(list([partition_range.Range(250, 500), partition_range.Range(600, 1000)])) self.assertEqual(3, len(read_collection_links)) self.assertTrue(collection_links[0] in read_collection_links) self.assertTrue(collection_links[1] in read_collection_links) self.assertTrue(collection_links[2] in read_collection_links) read_collection_links = rangepartition_resolver.ResolveForRead(list([50, 100, 600, 1000])) self.assertEqual(3, len(read_collection_links)) self.assertTrue(collection_links[0] in read_collection_links) self.assertTrue(collection_links[1] in read_collection_links) self.assertTrue(collection_links[2] in read_collection_links) read_collection_links = rangepartition_resolver.ResolveForRead(list([100, None])) self.assertEqual(3, len(read_collection_links)) self.assertTrue(collection_links[0] in read_collection_links) self.assertTrue(collection_links[1] in read_collection_links) self.assertTrue(collection_links[2] in read_collection_links) # Upsert test for Document resource - selflink version def test_document_upsert_self_link(self): self._test_document_upsert(False) # Upsert test for Document resource - name based routing version def test_document_upsert_name_based(self): self._test_document_upsert(True) def _test_document_upsert(self, is_name_based): # create database created_db = self.databseForTest # create collection created_collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # read documents and check count documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based))) before_create_documents_count = len(documents) # create document definition document_definition = {'id': 'doc', 'name': 'sample document', 'spam': 'eggs', 'key': 'value'} # create document using Upsert API created_document = self.client.UpsertItem( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), document_definition) # verify id property self.assertEqual(created_document['id'], document_definition['id']) # read documents after creation and verify updated count documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based))) self.assertEqual( len(documents), before_create_documents_count + 1, 'create should increase the number of documents') # update document created_document['name'] = 'replaced document' created_document['spam'] = 'not eggs' # should replace document since it already exists upserted_document = self.client.UpsertItem( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), created_document) # verify the changed properties self.assertEqual(upserted_document['name'], created_document['name'], 'document id property should change') self.assertEqual(upserted_document['spam'], created_document['spam'], 'property should have changed') # verify id property self.assertEqual(upserted_document['id'], created_document['id'], 'document id should stay the same') # read documents after upsert and verify count doesn't increases again documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based))) self.assertEqual( len(documents), before_create_documents_count + 1, 'number of documents should remain same') created_document['id'] = 'new id' # Upsert should create new document since the id is different new_document = self.client.UpsertItem( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based), created_document) # verify id property self.assertEqual(created_document['id'], new_document['id'], 'document id should be same') # read documents after upsert and verify count increases documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based))) self.assertEqual( len(documents), before_create_documents_count + 2, 'upsert should increase the number of documents') # delete documents self.client.DeleteItem(self.GetDocumentLink(created_db, created_collection, upserted_document, is_name_based)) self.client.DeleteItem(self.GetDocumentLink(created_db, created_collection, new_document, is_name_based)) # read documents after delete and verify count is same as original documents = list(self.client.ReadItems( self.GetDocumentCollectionLink(created_db, created_collection, is_name_based))) self.assertEqual( len(documents), before_create_documents_count, 'number of documents should remain same') def test_spatial_index_self_link(self): self._test_spatial_index(False) def test_spatial_index_name_based(self): self._test_spatial_index(True) def _test_spatial_index(self, is_name_based): db = self.databseForTest # partial policy specified collection = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), { 'id': 'collection with spatial index ' + str(uuid.uuid4()), 'indexingPolicy': { 'includedPaths': [ { 'path': '/"Location"/?', 'indexes': [ { 'kind': 'Spatial', 'dataType': 'Point' } ] }, { 'path': '/' } ] } }) self.client.CreateItem(self.GetDocumentCollectionLink(db, collection, is_name_based), { 'id': 'loc1', 'Location': { 'type': 'Point', 'coordinates': [ 20.0, 20.0 ] } }) self.client.CreateItem(self.GetDocumentCollectionLink(db, collection, is_name_based), { 'id': 'loc2', 'Location': { 'type': 'Point', 'coordinates': [ 100.0, 100.0 ] } }) results = list(self.client.QueryItems( self.GetDocumentCollectionLink(db, collection, is_name_based), "SELECT * FROM root WHERE (ST_DISTANCE(root.Location, {type: 'Point', coordinates: [20.1, 20]}) < 20000) ")) self.assertEqual(1, len(results)) self.assertEqual('loc1', results[0]['id']) def test_attachment_crud_self_link(self): self._test_attachment_crud(False) def test_attachment_crud_name_based(self): self._test_attachment_crud(True) def _test_attachment_crud(self, is_name_based): class ReadableStream(object): """Customized file-like stream. """ def __init__(self, chunks = ['first chunk ', 'second chunk']): """Initialization. :Parameters: - `chunks`: list """ if six.PY2: self._chunks = list(chunks) else: # python3: convert to bytes self._chunks = [chunk.encode() for chunk in chunks] def read(self, n=-1): """Simulates the read method in a file stream. :Parameters: - `n`: int :Returns: str or bytes """ if self._chunks: return self._chunks.pop(0) else: return '' def __len__(self): """To make len(ReadableStream) work. """ return sum([len(chunk) for chunk in self._chunks]) # Should do attachment CRUD operations successfully self.client.connection_policy.MediaReadMode = documents.MediaReadMode.Buffered # create database db = self.databseForTest # create collection collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # create document document = self.client.CreateItem(self.GetDocumentCollectionLink(db, collection, is_name_based), { 'id': 'sample document', 'spam': 'eggs', 'key': 'value' }) # list all attachments attachments = list(self.client.ReadAttachments(self.GetDocumentLink(db, collection, document, is_name_based))) initial_count = len(attachments) valid_media_options = { 'slug': 'attachment name', 'contentType': 'application/text' } invalid_media_options = { 'slug': 'attachment name', 'contentType': 'junt/test' } # create attachment with invalid content-type content_stream = ReadableStream() self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateAttachmentAndUploadMedia, self.GetDocumentLink(db, collection, document, is_name_based), content_stream, invalid_media_options) content_stream = ReadableStream() # create streamed attachment with valid content-type valid_attachment = self.client.CreateAttachmentAndUploadMedia( self.GetDocumentLink(db, collection, document, is_name_based), content_stream, valid_media_options) self.assertEqual(valid_attachment['id'], 'attachment name', 'id of created attachment should be the' ' same as the one in the request') content_stream = ReadableStream() # create colliding attachment self.__AssertHTTPFailureWithStatus( StatusCodes.CONFLICT, self.client.CreateAttachmentAndUploadMedia, self.GetDocumentLink(db, collection, document, is_name_based), content_stream, valid_media_options) content_stream = ReadableStream() # create attachment with media link dynamic_attachment = { 'id': 'dynamic attachment', 'media': 'http://xstore.', 'MediaType': 'Book', 'Author':'My Book Author', 'Title':'My Book Title', 'contentType':'application/text' } attachment = self.client.CreateAttachment(self.GetDocumentLink(db, collection, document, is_name_based), dynamic_attachment) self.assertEqual(attachment['MediaType'], 'Book', 'invalid media type') self.assertEqual(attachment['Author'], 'My Book Author', 'invalid property value') # list all attachments attachments = list(self.client.ReadAttachments(self.GetDocumentLink(db, collection, document, is_name_based))) self.assertEqual(len(attachments), initial_count + 2, 'number of attachments should\'ve increased by 2') attachment['Author'] = 'new author' # replace the attachment self.client.ReplaceAttachment(self.GetAttachmentLink(db, collection, document, attachment, is_name_based), attachment) self.assertEqual(attachment['MediaType'], 'Book', 'invalid media type') self.assertEqual(attachment['Author'], 'new author', 'invalid property value') # read attachment media media_response = self.client.ReadMedia(valid_attachment['media']) self.assertEqual(media_response, 'first chunk second chunk') content_stream = ReadableStream(['modified first chunk ', 'modified second chunk']) # update attachment media self.client.UpdateMedia(valid_attachment['media'], content_stream, valid_media_options) # read attachment media after update # read media buffered media_response = self.client.ReadMedia(valid_attachment['media']) self.assertEqual(media_response, 'modified first chunk modified second chunk') # read media streamed self.client.connection_policy.MediaReadMode = ( documents.MediaReadMode.Streamed) media_response = self.client.ReadMedia(valid_attachment['media']) self.assertEqual(media_response.read(), b'modified first chunk modified second chunk') # share attachment with a second document document = self.client.CreateItem(self.GetDocumentCollectionLink(db, collection, is_name_based), {'id': 'document 2'}) second_attachment = { 'id': valid_attachment['id'], 'contentType': valid_attachment['contentType'], 'media': valid_attachment['media'] } attachment = self.client.CreateAttachment(self.GetDocumentLink(db, collection, document, is_name_based), second_attachment) self.assertEqual(valid_attachment['id'], attachment['id'], 'id mismatch') self.assertEqual(valid_attachment['media'], attachment['media'], 'media mismatch') self.assertEqual(valid_attachment['contentType'], attachment['contentType'], 'contentType mismatch') # deleting attachment self.client.DeleteAttachment(self.GetAttachmentLink(db, collection, document, attachment, is_name_based)) # read attachments after deletion attachments = list(self.client.ReadAttachments(self.GetDocumentLink(db, collection, document, is_name_based))) self.assertEqual(len(attachments), 0) # Upsert test for Attachment resource - selflink version def test_attachment_upsert_self_link(self): self._test_attachment_upsert(False) # Upsert test for Attachment resource - name based routing version def test_attachment_upsert_name_based(self): self._test_attachment_upsert(True) def _test_attachment_upsert(self, is_name_based): class ReadableStream(object): """Customized file-like stream. """ def __init__(self, chunks = ['first chunk ', 'second chunk']): """Initialization. :Parameters: - `chunks`: list """ if six.PY2: self._chunks = list(chunks) else: # python3: convert to bytes self._chunks = [chunk.encode() for chunk in chunks] def read(self, n=-1): """Simulates the read method in a file stream. :Parameters: - `n`: int :Returns: str or bytes """ if self._chunks: return self._chunks.pop(0) else: return '' def __len__(self): """To make len(ReadableStream) work. """ return sum([len(chunk) for chunk in self._chunks]) # create database db = self.databseForTest # create collection collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # create document document = self.client.CreateItem(self.GetDocumentCollectionLink(db, collection, is_name_based), { 'id': 'sample document' + str(uuid.uuid4()), 'spam': 'eggs', 'key': 'value' }) # list all attachments and check count attachments = list(self.client.ReadAttachments(self.GetDocumentLink(db, collection, document, is_name_based))) initial_count = len(attachments) valid_media_options = { 'slug': 'attachment name', 'contentType': 'application/text' } content_stream = ReadableStream() # create streamed attachment with valid content-type using Upsert API valid_attachment = self.client.UpsertAttachmentAndUploadMedia( self.GetDocumentLink(db, collection, document, is_name_based), content_stream, valid_media_options) # verify id property self.assertEqual(valid_attachment['id'], 'attachment name', 'id of created attachment should be the same') valid_media_options = { 'slug': 'new attachment name', 'contentType': 'application/text' } content_stream = ReadableStream() # Upsert should create new attachment since since id is different new_valid_attachment = self.client.UpsertAttachmentAndUploadMedia( self.GetDocumentLink(db, collection, document, is_name_based), content_stream, valid_media_options) # verify id property self.assertEqual(new_valid_attachment['id'], 'new attachment name', 'id of new attachment should be the same') # read all attachments and verify updated count attachments = list(self.client.ReadAttachments(self.GetDocumentLink(db, collection, document, is_name_based))) self.assertEqual(len(attachments), initial_count + 2, 'number of attachments should have increased by 2') # create attachment with media link attachment_definition = { 'id': 'dynamic attachment', 'media': 'http://xstore.', 'MediaType': 'Book', 'Author':'My Book Author', 'Title':'My Book Title', 'contentType':'application/text' } # create dynamic attachment using Upsert API dynamic_attachment = self.client.UpsertAttachment(self.GetDocumentLink(db, collection, document, is_name_based), attachment_definition) # verify id property self.assertEqual(dynamic_attachment['id'], attachment_definition['id'], 'id of attachment should be the same') # read all attachments and verify updated count attachments = list(self.client.ReadAttachments(self.GetDocumentLink(db, collection, document, is_name_based))) self.assertEqual(len(attachments), initial_count + 3, 'number of attachments should have increased by 3') dynamic_attachment['Author'] = 'new author' # replace the attachment using Upsert API upserted_attachment = self.client.UpsertAttachment(self.GetDocumentLink(db, collection, document, is_name_based), dynamic_attachment) # verify id property remains same self.assertEqual(dynamic_attachment['id'], upserted_attachment['id'], 'id should stay the same') # verify author property gets updated self.assertEqual(upserted_attachment['Author'], dynamic_attachment['Author'], 'invalid property value') # read all attachments and verify count doesn't increases again attachments = list(self.client.ReadAttachments(self.GetDocumentLink(db, collection, document, is_name_based))) self.assertEqual(len(attachments), initial_count + 3, 'number of attachments should remain same') dynamic_attachment['id'] = 'new dynamic attachment' # Upsert should create new attachment since id is different new_attachment = self.client.UpsertAttachment(self.GetDocumentLink(db, collection, document, is_name_based), dynamic_attachment) # verify id property remains same self.assertEqual(dynamic_attachment['id'], new_attachment['id'], 'id should be same') # read all attachments and verify count increases attachments = list(self.client.ReadAttachments(self.GetDocumentLink(db, collection, document, is_name_based))) self.assertEqual(len(attachments), initial_count + 4, 'number of attachments should have increased') # deleting attachments self.client.DeleteAttachment(self.GetAttachmentLink(db, collection, document, valid_attachment, is_name_based)) self.client.DeleteAttachment(self.GetAttachmentLink(db, collection, document, new_valid_attachment, is_name_based)) self.client.DeleteAttachment(self.GetAttachmentLink(db, collection, document, upserted_attachment, is_name_based)) self.client.DeleteAttachment(self.GetAttachmentLink(db, collection, document, new_attachment, is_name_based)) # wait to ensure deletes are propagated for multimaster enabled accounts if self.configs.IS_MULTIMASTER_ENABLED: time.sleep(2) # read all attachments and verify count remains same attachments = list(self.client.ReadAttachments(self.GetDocumentLink(db, collection, document, is_name_based))) self.assertEqual(len(attachments), initial_count, 'number of attachments should remain the same') def test_user_crud_self_link(self): self._test_user_crud(False) def test_user_crud_name_based(self): self._test_user_crud(True) def _test_user_crud(self, is_name_based): # Should do User CRUD operations successfully. # create database db = self.databseForTest # list users users = list(self.client.ReadUsers(self.GetDatabaseLink(db, is_name_based))) before_create_count = len(users) # create user user_id = 'new user' + str(uuid.uuid4()) user = self.client.CreateUser(self.GetDatabaseLink(db, is_name_based), { 'id': user_id }) self.assertEqual(user['id'], user_id, 'user id error') # list users after creation users = list(self.client.ReadUsers(self.GetDatabaseLink(db, is_name_based))) self.assertEqual(len(users), before_create_count + 1) # query users results = list(self.client.QueryUsers( self.GetDatabaseLink(db, is_name_based), { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name':'@id', 'value':user_id } ] })) self.assertTrue(results) # replace user change_user = user.copy() replaced_user_id = 'replaced user' + str(uuid.uuid4()) user['id'] = replaced_user_id replaced_user = self.client.ReplaceUser(self.GetUserLink(db, change_user, is_name_based), user) self.assertEqual(replaced_user['id'], replaced_user_id, 'user id should change') self.assertEqual(user['id'], replaced_user['id'], 'user id should stay the same') # read user user = self.client.ReadUser(self.GetUserLink(db, replaced_user, is_name_based)) self.assertEqual(replaced_user['id'], user['id']) # delete user self.client.DeleteUser(self.GetUserLink(db, user, is_name_based)) # read user after deletion self.__AssertHTTPFailureWithStatus(StatusCodes.NOT_FOUND, self.client.ReadUser, self.GetUserLink(db, user, is_name_based)) # Upsert test for User resource - selflink version def test_user_upsert_self_link(self): self._test_user_upsert(False) # Upsert test for User resource - named based routing version def test_user_upsert_name_based(self): self._test_user_upsert(True) def _test_user_upsert(self, is_name_based): # create database db = self.databseForTest # read users and check count users = list(self.client.ReadUsers(self.GetDatabaseLink(db, is_name_based))) before_create_count = len(users) # create user using Upsert API user_id = 'user' + str(uuid.uuid4()) user = self.client.UpsertUser(self.GetDatabaseLink(db, is_name_based), { 'id': user_id }) # verify id property self.assertEqual(user['id'], user_id, 'user id error') # read users after creation and verify updated count users = list(self.client.ReadUsers(self.GetDatabaseLink(db, is_name_based))) self.assertEqual(len(users), before_create_count + 1) # Should replace the user since it already exists, there is no public property to change here upserted_user = self.client.UpsertUser(self.GetDatabaseLink(db, is_name_based), user) # verify id property self.assertEqual(upserted_user['id'], user['id'], 'user id should remain same') # read users after upsert and verify count doesn't increases again users = list(self.client.ReadUsers(self.GetDatabaseLink(db, is_name_based))) self.assertEqual(len(users), before_create_count + 1) user['id'] = 'new user' + str(uuid.uuid4()) # Upsert should create new user since id is different new_user = self.client.UpsertUser(self.GetDatabaseLink(db, is_name_based), user) # verify id property self.assertEqual(new_user['id'], user['id'], 'user id error') # read users after upsert and verify count increases users = list(self.client.ReadUsers(self.GetDatabaseLink(db, is_name_based))) self.assertEqual(len(users), before_create_count + 2) # delete users self.client.DeleteUser(self.GetUserLink(db, upserted_user, is_name_based)) self.client.DeleteUser(self.GetUserLink(db, new_user, is_name_based)) # read users after delete and verify count remains the same users = list(self.client.ReadUsers(self.GetDatabaseLink(db, is_name_based))) self.assertEqual(len(users), before_create_count) def test_permission_crud_self_link(self): self._test_permission_crud(False) def test_permission_crud_name_based(self): self._test_permission_crud(True) def _test_permission_crud(self, is_name_based): # Should do Permission CRUD operations successfully # create database db = self.databseForTest # create user user = self.client.CreateUser(self.GetDatabaseLink(db, is_name_based), { 'id': 'new user' + str(uuid.uuid4())}) # list permissions permissions = list(self.client.ReadPermissions(self.GetUserLink(db, user, is_name_based))) before_create_count = len(permissions) permission = { 'id': 'new permission', 'permissionMode': documents.PermissionMode.Read, 'resource': 'dbs/AQAAAA==/colls/AQAAAJ0fgTc=' # A random one. } # create permission permission = self.client.CreatePermission(self.GetUserLink(db, user, is_name_based), permission) self.assertEqual(permission['id'], 'new permission', 'permission id error') # list permissions after creation permissions = list(self.client.ReadPermissions(self.GetUserLink(db, user, is_name_based))) self.assertEqual(len(permissions), before_create_count + 1) # query permissions results = list(self.client.QueryPermissions( self.GetUserLink(db, user, is_name_based), { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name':'@id', 'value':permission['id'] } ] })) self.assert_(results) # replace permission change_permission = permission.copy() permission['id'] = 'replaced permission' replaced_permission = self.client.ReplacePermission(self.GetPermissionLink(db, user, change_permission, is_name_based), permission) self.assertEqual(replaced_permission['id'], 'replaced permission', 'permission id should change') self.assertEqual(permission['id'], replaced_permission['id'], 'permission id should stay the same') # read permission permission = self.client.ReadPermission(self.GetPermissionLink(db, user, replaced_permission, is_name_based)) self.assertEqual(replaced_permission['id'], permission['id']) # delete permission self.client.DeletePermission(self.GetPermissionLink(db, user, replaced_permission, is_name_based)) # read permission after deletion self.__AssertHTTPFailureWithStatus(StatusCodes.NOT_FOUND, self.client.ReadPermission, self.GetPermissionLink(db, user, permission, is_name_based)) # Upsert test for Permission resource - selflink version def test_permission_upsert_self_link(self): self._test_permission_upsert(False) # Upsert test for Permission resource - name based routing version def test_permission_upsert_name_based(self): self._test_permission_upsert(True) def _test_permission_upsert(self, is_name_based): # create database db = self.databseForTest # create user user = self.client.CreateUser(self.GetDatabaseLink(db, is_name_based), { 'id': 'new user' + str(uuid.uuid4())}) # read permissions and check count permissions = list(self.client.ReadPermissions(self.GetUserLink(db, user, is_name_based))) before_create_count = len(permissions) permission_definition = { 'id': 'permission', 'permissionMode': documents.PermissionMode.Read, 'resource': 'dbs/AQAAAA==/colls/AQAAAJ0fgTc=' # A random one. } # create permission using Upsert API created_permission = self.client.UpsertPermission(self.GetUserLink(db, user, is_name_based), permission_definition) # verify id property self.assertEqual(created_permission['id'], permission_definition['id'], 'permission id error') # read permissions after creation and verify updated count permissions = list(self.client.ReadPermissions(self.GetUserLink(db, user, is_name_based))) self.assertEqual(len(permissions), before_create_count + 1) # update permission mode permission_definition['permissionMode'] = documents.PermissionMode.All # should repace the permission since it already exists upserted_permission = self.client.UpsertPermission(self.GetUserLink(db, user, is_name_based), permission_definition) # verify id property self.assertEqual(upserted_permission['id'], created_permission['id'], 'permission id should remain same') # verify changed property self.assertEqual(upserted_permission['permissionMode'], permission_definition['permissionMode'], 'permissionMode should change') # read permissions and verify count doesn't increases again permissions = list(self.client.ReadPermissions(self.GetUserLink(db, user, is_name_based))) self.assertEqual(len(permissions), before_create_count + 1) # update permission id created_permission['id'] = 'new permission' # resource needs to be changed along with the id in order to create a new permission created_permission['resource'] = 'dbs/N9EdAA==/colls/N9EdAIugXgA=' # should create new permission since id has changed new_permission = self.client.UpsertPermission(self.GetUserLink(db, user, is_name_based), created_permission) # verify id and resource property self.assertEqual(new_permission['id'], created_permission['id'], 'permission id should be same') self.assertEqual(new_permission['resource'], created_permission['resource'], 'permission resource should be same') # read permissions and verify count increases permissions = list(self.client.ReadPermissions(self.GetUserLink(db, user, is_name_based))) self.assertEqual(len(permissions), before_create_count + 2) # delete permissions self.client.DeletePermission(self.GetPermissionLink(db, user, upserted_permission, is_name_based)) self.client.DeletePermission(self.GetPermissionLink(db, user, new_permission, is_name_based)) # read permissions and verify count remains the same permissions = list(self.client.ReadPermissions(self.GetUserLink(db, user, is_name_based))) self.assertEqual(len(permissions), before_create_count) def test_authorization(self): def __SetupEntities(client): """ Sets up entities for this test. :Parameters: - `client`: cosmos_client.CosmosClient :Returns: dict """ # create database db = self.databseForTest # create collection1 collection1 = client.CreateContainer( db['_self'], { 'id': 'test_authorization ' + str(uuid.uuid4()) }) # create document1 document1 = client.CreateItem(collection1['_self'], { 'id': 'coll1doc1', 'spam': 'eggs', 'key': 'value' }) # create document 2 document2 = client.CreateItem( collection1['_self'], { 'id': 'coll1doc2', 'spam': 'eggs2', 'key': 'value2' }) # create attachment dynamic_attachment = { 'id': 'dynamic attachment', 'media': 'http://xstore.', 'MediaType': 'Book', 'Author': 'My Book Author', 'Title': 'My Book Title', 'contentType': 'application/text' } attachment = client.CreateAttachment(document1['_self'], dynamic_attachment) # create collection 2 collection2 = client.CreateContainer( db['_self'], { 'id': 'test_authorization2 ' + str(uuid.uuid4()) }) # create user1 user1 = client.CreateUser(db['_self'], { 'id': 'user1' }) permission = { 'id': 'permission On Coll1', 'permissionMode': documents.PermissionMode.Read, 'resource': collection1['_self'] } # create permission for collection1 permission_on_coll1 = client.CreatePermission(user1['_self'], permission) self.assertTrue(permission_on_coll1['_token'] != None, 'permission token is invalid') permission = { 'id': 'permission On Doc1', 'permissionMode': documents.PermissionMode.All, 'resource': document2['_self'] } # create permission for document 2 permission_on_doc2 = client.CreatePermission(user1['_self'], permission) self.assertTrue(permission_on_doc2['_token'] != None, 'permission token is invalid') # create user 2 user2 = client.CreateUser(db['_self'], { 'id': 'user2' }) permission = { 'id': 'permission On coll2', 'permissionMode': documents.PermissionMode.All, 'resource': collection2['_self'] } # create permission on collection 2 permission_on_coll2 = client.CreatePermission( user2['_self'], permission) entities = { 'db': db, 'coll1': collection1, 'coll2': collection2, 'doc1': document1, 'doc2': document2, 'user1': user1, 'user2': user2, 'attachment': attachment, 'permissionOnColl1': permission_on_coll1, 'permissionOnDoc2': permission_on_doc2, 'permissionOnColl2': permission_on_coll2 } return entities # Client without any authorization will fail. client = cosmos_client.CosmosClient(CRUDTests.host, {}, CRUDTests.connectionPolicy) self.__AssertHTTPFailureWithStatus(StatusCodes.UNAUTHORIZED, list, client.ReadDatabases()) # Client with master key. client = cosmos_client.CosmosClient(CRUDTests.host, {'masterKey': CRUDTests.masterKey}, CRUDTests.connectionPolicy) # setup entities entities = __SetupEntities(client) resource_tokens = {} resource_tokens[entities['coll1']['_rid']] = ( entities['permissionOnColl1']['_token']) resource_tokens[entities['doc1']['_rid']] = ( entities['permissionOnColl1']['_token']) col1_client = cosmos_client.CosmosClient( CRUDTests.host, {'resourceTokens': resource_tokens}, CRUDTests.connectionPolicy) # 1. Success-- Use Col1 Permission to Read success_coll1 = col1_client.ReadContainer( entities['coll1']['_self']) # 2. Failure-- Use Col1 Permission to delete self.__AssertHTTPFailureWithStatus(StatusCodes.FORBIDDEN, col1_client.DeleteContainer, success_coll1['_self']) # 3. Success-- Use Col1 Permission to Read All Docs success_documents = list(col1_client.ReadItems( success_coll1['_self'])) self.assertTrue(success_documents != None, 'error reading documents') self.assertEqual(len(success_documents), 2, 'Expected 2 Documents to be succesfully read') # 4. Success-- Use Col1 Permission to Read Col1Doc1 success_doc = col1_client.ReadItem(entities['doc1']['_self']) self.assertTrue(success_doc != None, 'error reading document') self.assertEqual( success_doc['id'], entities['doc1']['id'], 'Expected to read children using parent permissions') col2_client = cosmos_client.CosmosClient( CRUDTests.host, { 'permissionFeed': [ entities['permissionOnColl2'] ] }, CRUDTests.connectionPolicy) doc = { 'CustomProperty1': 'BBBBBB', 'customProperty2': 1000, 'id': entities['doc2']['id'] } success_doc = col2_client.CreateItem( entities['coll2']['_self'], doc) self.assertTrue(success_doc != None, 'error creating document') self.assertEqual(success_doc['CustomProperty1'], doc['CustomProperty1'], 'document should have been created successfully') self.client.DeleteContainer(entities['coll1']['_self']) self.client.DeleteContainer(entities['coll2']['_self']) def test_trigger_crud_self_link(self): self._test_trigger_crud(False) def test_trigger_crud_name_based(self): self._test_trigger_crud(True) def _test_trigger_crud(self, is_name_based): # create database db = self.databseForTest # create collection collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # read triggers triggers = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection, is_name_based))) # create a trigger before_create_triggers_count = len(triggers) trigger_definition = { 'id': 'sample trigger', 'serverScript': 'function() {var x = 10;}', 'triggerType': documents.TriggerType.Pre, 'triggerOperation': documents.TriggerOperation.All } trigger = self.client.CreateTrigger(self.GetDocumentCollectionLink(db, collection, is_name_based), trigger_definition) for property in trigger_definition: if property != "serverScript": self.assertEqual( trigger[property], trigger_definition[property], 'property {property} should match'.format(property=property)) else: self.assertEqual(trigger['body'], 'function() {var x = 10;}') # read triggers after creation triggers = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(triggers), before_create_triggers_count + 1, 'create should increase the number of triggers') # query triggers triggers = list(self.client.QueryTriggers( self.GetDocumentCollectionLink(db, collection, is_name_based), { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name': '@id', 'value': trigger_definition['id']} ] })) self.assert_(triggers) # replace trigger change_trigger = trigger.copy() trigger['body'] = 'function() {var x = 20;}' replaced_trigger = self.client.ReplaceTrigger(self.GetTriggerLink(db, collection, change_trigger, is_name_based), trigger) for property in trigger_definition: if property != "serverScript": self.assertEqual( replaced_trigger[property], trigger[property], 'property {property} should match'.format(property=property)) else: self.assertEqual(replaced_trigger['body'], 'function() {var x = 20;}') # read trigger trigger = self.client.ReadTrigger(self.GetTriggerLink(db, collection, replaced_trigger, is_name_based)) self.assertEqual(replaced_trigger['id'], trigger['id']) # delete trigger self.client.DeleteTrigger(self.GetTriggerLink(db, collection, replaced_trigger, is_name_based)) # read triggers after deletion self.__AssertHTTPFailureWithStatus(StatusCodes.NOT_FOUND, self.client.ReadTrigger, self.GetTriggerLink(db, collection, replaced_trigger, is_name_based)) # Upsert test for Trigger resource - selflink version def test_trigger_upsert_self_link(self): self._test_trigger_upsert(False) # Upsert test for Trigger resource - name based routing version def test_trigger_upsert_name_based(self): self._test_trigger_upsert(True) def _test_trigger_upsert(self, is_name_based): # create database db = self.databseForTest # create collection collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # read triggers and check count triggers = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection, is_name_based))) before_create_triggers_count = len(triggers) # create a trigger trigger_definition = { 'id': 'sample trigger', 'serverScript': 'function() {var x = 10;}', 'triggerType': documents.TriggerType.Pre, 'triggerOperation': documents.TriggerOperation.All } # create trigger using Upsert API created_trigger = self.client.UpsertTrigger(self.GetDocumentCollectionLink(db, collection, is_name_based), trigger_definition) # verify id property self.assertEqual(created_trigger['id'], trigger_definition['id']) # read triggers after creation and verify updated count triggers = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(triggers), before_create_triggers_count + 1, 'create should increase the number of triggers') # update trigger created_trigger['body'] = 'function() {var x = 20;}' # should replace trigger since it already exists upserted_trigger = self.client.UpsertTrigger(self.GetDocumentCollectionLink(db, collection, is_name_based), created_trigger) # verify id property self.assertEqual(created_trigger['id'], upserted_trigger['id']) # verify changed properties self.assertEqual(upserted_trigger['body'], created_trigger['body']) # read triggers after upsert and verify count remains same triggers = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(triggers), before_create_triggers_count + 1, 'upsert should keep the number of triggers same') # update trigger created_trigger['id'] = 'new trigger' # should create new trigger since id is changed new_trigger = self.client.UpsertTrigger(self.GetDocumentCollectionLink(db, collection, is_name_based), created_trigger) # verify id property self.assertEqual(created_trigger['id'], new_trigger['id']) # read triggers after upsert and verify count increases triggers = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(triggers), before_create_triggers_count + 2, 'upsert should increase the number of triggers') # delete triggers self.client.DeleteTrigger(self.GetTriggerLink(db, collection, upserted_trigger, is_name_based)) self.client.DeleteTrigger(self.GetTriggerLink(db, collection, new_trigger, is_name_based)) # read triggers after delete and verify count remains the same triggers = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(triggers), before_create_triggers_count, 'delete should bring the number of triggers to original') def test_udf_crud_self_link(self): self._test_udf_crud(False) def test_udf_crud_name_based(self): self._test_udf_crud(True) def _test_udf_crud(self, is_name_based): # create database db = self.databseForTest # create collection collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # read udfs udfs = list(self.client.ReadUserDefinedFunctions(self.GetDocumentCollectionLink(db, collection, is_name_based))) # create a udf before_create_udfs_count = len(udfs) udf_definition = { 'id': 'sample udf', 'body': 'function() {var x = 10;}' } udf = self.client.CreateUserDefinedFunction(self.GetDocumentCollectionLink(db, collection, is_name_based), udf_definition) for property in udf_definition: self.assertEqual( udf[property], udf_definition[property], 'property {property} should match'.format(property=property)) # read udfs after creation udfs = list(self.client.ReadUserDefinedFunctions(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(udfs), before_create_udfs_count + 1, 'create should increase the number of udfs') # query udfs results = list(self.client.QueryUserDefinedFunctions( self.GetDocumentCollectionLink(db, collection, is_name_based), { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ {'name':'@id', 'value':udf_definition['id']} ] })) self.assert_(results) # replace udf change_udf = udf.copy() udf['body'] = 'function() {var x = 20;}' replaced_udf = self.client.ReplaceUserDefinedFunction(self.GetUserDefinedFunctionLink(db, collection, change_udf, is_name_based), udf) for property in udf_definition: self.assertEqual( replaced_udf[property], udf[property], 'property {property} should match'.format(property=property)) # read udf udf = self.client.ReadUserDefinedFunction(self.GetUserDefinedFunctionLink(db, collection, replaced_udf, is_name_based)) self.assertEqual(replaced_udf['id'], udf['id']) # delete udf self.client.DeleteUserDefinedFunction(self.GetUserDefinedFunctionLink(db, collection, replaced_udf, is_name_based)) # read udfs after deletion self.__AssertHTTPFailureWithStatus(StatusCodes.NOT_FOUND, self.client.ReadUserDefinedFunction, self.GetUserDefinedFunctionLink(db, collection, replaced_udf, is_name_based)) # Upsert test for User Defined Function resource - selflink version def test_udf_upsert_self_link(self): self._test_udf_upsert(False) # Upsert test for User Defined Function resource - name based routing version def test_udf_upsert_name_based(self): self._test_udf_upsert(True) def _test_udf_upsert(self, is_name_based): # create database db = self.databseForTest # create collection collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # read udfs and check count udfs = list(self.client.ReadUserDefinedFunctions(self.GetDocumentCollectionLink(db, collection, is_name_based))) before_create_udfs_count = len(udfs) # create a udf definition udf_definition = { 'id': 'sample udf', 'body': 'function() {var x = 10;}' } # create udf using Upsert API created_udf = self.client.UpsertUserDefinedFunction(self.GetDocumentCollectionLink(db, collection, is_name_based), udf_definition) # verify id property self.assertEqual(created_udf['id'], udf_definition['id']) # read udfs after creation and verify updated count udfs = list(self.client.ReadUserDefinedFunctions(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(udfs), before_create_udfs_count + 1, 'create should increase the number of udfs') # update udf created_udf['body'] = 'function() {var x = 20;}' # should replace udf since it already exists upserted_udf = self.client.UpsertUserDefinedFunction(self.GetDocumentCollectionLink(db, collection, is_name_based), created_udf) # verify id property self.assertEqual(created_udf['id'], upserted_udf['id']) # verify changed property self.assertEqual(upserted_udf['body'], created_udf['body']) # read udf and verify count doesn't increases again udfs = list(self.client.ReadUserDefinedFunctions(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(udfs), before_create_udfs_count + 1, 'upsert should keep the number of udfs same') created_udf['id'] = 'new udf' # should create new udf since the id is different new_udf = self.client.UpsertUserDefinedFunction(self.GetDocumentCollectionLink(db, collection, is_name_based), created_udf) # verify id property self.assertEqual(created_udf['id'], new_udf['id']) # read udf and verify count increases udfs = list(self.client.ReadUserDefinedFunctions(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(udfs), before_create_udfs_count + 2, 'upsert should keep the number of udfs same') # delete udfs self.client.DeleteUserDefinedFunction(self.GetUserDefinedFunctionLink(db, collection, upserted_udf, is_name_based)) self.client.DeleteUserDefinedFunction(self.GetUserDefinedFunctionLink(db, collection, new_udf, is_name_based)) # read udf and verify count remains the same udfs = list(self.client.ReadUserDefinedFunctions(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(udfs), before_create_udfs_count, 'delete should keep the number of udfs same') def test_sproc_crud_self_link(self): self._test_sproc_crud(False) def test_sproc_crud_name_based(self): self._test_sproc_crud(True) def _test_sproc_crud(self, is_name_based): # create database db = self.databseForTest # create collection collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # read sprocs sprocs = list(self.client.ReadStoredProcedures(self.GetDocumentCollectionLink(db, collection, is_name_based))) # create a sproc before_create_sprocs_count = len(sprocs) sproc_definition = { 'id': 'sample sproc', 'serverScript': 'function() {var x = 10;}' } sproc = self.client.CreateStoredProcedure(self.GetDocumentCollectionLink(db, collection, is_name_based), sproc_definition) for property in sproc_definition: if property != "serverScript": self.assertEqual( sproc[property], sproc_definition[property], 'property {property} should match'.format(property=property)) else: self.assertEqual(sproc['body'], 'function() {var x = 10;}') # read sprocs after creation sprocs = list(self.client.ReadStoredProcedures(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(sprocs), before_create_sprocs_count + 1, 'create should increase the number of sprocs') # query sprocs sprocs = list(self.client.QueryStoredProcedures( self.GetDocumentCollectionLink(db, collection, is_name_based), { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters':[ { 'name':'@id', 'value':sproc_definition['id'] } ] })) self.assert_(sprocs) # replace sproc change_sproc = sproc.copy() sproc['body'] = 'function() {var x = 20;}' replaced_sproc = self.client.ReplaceStoredProcedure(self.GetStoredProcedureLink(db, collection, change_sproc, is_name_based), sproc) for property in sproc_definition: if property != 'serverScript': self.assertEqual( replaced_sproc[property], sproc[property], 'property {property} should match'.format(property=property)) else: self.assertEqual(replaced_sproc['body'], "function() {var x = 20;}") # read sproc sproc = self.client.ReadStoredProcedure(self.GetStoredProcedureLink(db, collection, replaced_sproc, is_name_based)) self.assertEqual(replaced_sproc['id'], sproc['id']) # delete sproc self.client.DeleteStoredProcedure(self.GetStoredProcedureLink(db, collection, replaced_sproc, is_name_based)) # read sprocs after deletion self.__AssertHTTPFailureWithStatus(StatusCodes.NOT_FOUND, self.client.ReadStoredProcedure, self.GetStoredProcedureLink(db, collection, replaced_sproc, is_name_based)) # Upsert test for sproc resource - selflink version def test_sproc_upsert_self_link(self): self._test_sproc_upsert(False) # Upsert test for sproc resource - name based routing version def test_sproc_upsert_name_based(self): self._test_sproc_upsert(True) def _test_sproc_upsert(self, is_name_based): # create database db = self.databseForTest # create collection collection = self.configs.create_single_partition_collection_if_not_exist(self.client) # read sprocs and check count sprocs = list(self.client.ReadStoredProcedures(self.GetDocumentCollectionLink(db, collection, is_name_based))) before_create_sprocs_count = len(sprocs) # create a sproc definition sproc_definition = { 'id': 'sample sproc', 'serverScript': 'function() {var x = 10;}' } # create sproc using Upsert API created_sproc = self.client.UpsertStoredProcedure(self.GetDocumentCollectionLink(db, collection, is_name_based), sproc_definition) # verify id property self.assertEqual(created_sproc['id'], sproc_definition['id']) # read sprocs after creation and verify updated count sprocs = list(self.client.ReadStoredProcedures(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(sprocs), before_create_sprocs_count + 1, 'create should increase the number of sprocs') # update sproc created_sproc['body'] = 'function() {var x = 20;}' # should replace sproc since it already exists upserted_sproc = self.client.UpsertStoredProcedure(self.GetDocumentCollectionLink(db, collection, is_name_based), created_sproc) # verify id property self.assertEqual(created_sproc['id'], upserted_sproc['id']) # verify changed property self.assertEqual(upserted_sproc['body'], created_sproc['body']) # read sprocs after upsert and verify count remains the same sprocs = list(self.client.ReadStoredProcedures(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(sprocs), before_create_sprocs_count + 1, 'upsert should keep the number of sprocs same') # update sproc created_sproc['id'] = 'new sproc' # should create new sproc since id is different new_sproc = self.client.UpsertStoredProcedure(self.GetDocumentCollectionLink(db, collection, is_name_based), created_sproc) # verify id property self.assertEqual(created_sproc['id'], new_sproc['id']) # read sprocs after upsert and verify count increases sprocs = list(self.client.ReadStoredProcedures(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(sprocs), before_create_sprocs_count + 2, 'upsert should keep the number of sprocs same') # delete sprocs self.client.DeleteStoredProcedure(self.GetStoredProcedureLink(db, collection, upserted_sproc, is_name_based)) self.client.DeleteStoredProcedure(self.GetStoredProcedureLink(db, collection, new_sproc, is_name_based)) # read sprocs after delete and verify count remains same sprocs = list(self.client.ReadStoredProcedures(self.GetDocumentCollectionLink(db, collection, is_name_based))) self.assertEqual(len(sprocs), before_create_sprocs_count, 'delete should keep the number of sprocs same') def test_scipt_logging_execute_stored_procedure(self): created_db = self.databseForTest created_collection = self.configs.create_single_partition_collection_if_not_exist(self.client) sproc = { 'id': 'storedProcedure' + str(uuid.uuid4()), 'body': ( 'function () {' + ' var mytext = \'x\';' + ' var myval = 1;' + ' try {' + ' console.log(\'The value of %s is %s.\', mytext, myval);' + ' getContext().getResponse().setBody(\'Success!\');' + ' }' + ' catch (err) {' + ' getContext().getResponse().setBody(\'inline err: [\' + err.number + \'] \' + err);' + ' }' '}') } created_sproc = self.client.CreateStoredProcedure(self.GetDocumentCollectionLink(created_db, created_collection), sproc) result = self.client.ExecuteStoredProcedure(self.GetStoredProcedureLink(created_db, created_collection, created_sproc), None) self.assertEqual(result, 'Success!') self.assertFalse(HttpHeaders.ScriptLogResults in self.client.last_response_headers) options = { 'enableScriptLogging': True } result = self.client.ExecuteStoredProcedure(self.GetStoredProcedureLink(created_db, created_collection, created_sproc), None, options) self.assertEqual(result, 'Success!') self.assertEqual(urllib.quote('The value of x is 1.'), self.client.last_response_headers.get(HttpHeaders.ScriptLogResults)) options = { 'enableScriptLogging': False } result = self.client.ExecuteStoredProcedure(self.GetStoredProcedureLink(created_db, created_collection, created_sproc), None, options) self.assertEqual(result, 'Success!') self.assertFalse(HttpHeaders.ScriptLogResults in self.client.last_response_headers) def test_collection_indexing_policy_self_link(self): self._test_collection_indexing_policy(False) def test_collection_indexing_policy_name_based(self): self._test_collection_indexing_policy(True) def _test_collection_indexing_policy(self, is_name_based): # create database db = self.databseForTest # create collection collection = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), { 'id': 'test_collection_indexing_policy default policy' + str(uuid.uuid4()) }) self.assertEqual(collection['indexingPolicy']['indexingMode'], documents.IndexingMode.Consistent, 'default indexing mode should be consistent') lazy_collection_definition = { 'id': 'test_collection_indexing_policy lazy collection ' + str(uuid.uuid4()), 'indexingPolicy': { 'indexingMode': documents.IndexingMode.Lazy } } self.client.DeleteContainer(self.GetDocumentCollectionLink(db, collection, is_name_based)) lazy_collection = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), lazy_collection_definition) self.assertEqual(lazy_collection['indexingPolicy']['indexingMode'], documents.IndexingMode.Lazy, 'indexing mode should be lazy') consistent_collection_definition = { 'id': 'test_collection_indexing_policy consistent collection ' + str(uuid.uuid4()), 'indexingPolicy': { 'indexingMode': documents.IndexingMode.Consistent } } self.client.DeleteContainer(self.GetDocumentCollectionLink(db, lazy_collection, is_name_based)) consistent_collection = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), consistent_collection_definition) self.assertEqual(collection['indexingPolicy']['indexingMode'], documents.IndexingMode.Consistent, 'indexing mode should be consistent') collection_definition = { 'id': 'CollectionWithIndexingPolicy', 'indexingPolicy': { 'automatic': True, 'indexingMode': documents.IndexingMode.Lazy, 'includedPaths': [ { 'path': '/', 'indexes': [ { 'kind': documents.IndexKind.Hash, 'dataType': documents.DataType.Number, 'precision': 2 } ] } ], 'excludedPaths': [ { 'path': '/"systemMetadata"/*' } ] } } self.client.DeleteContainer(self.GetDocumentCollectionLink(db, consistent_collection, is_name_based)) collection_with_indexing_policy = self.client.CreateContainer(self.GetDatabaseLink(db, is_name_based), collection_definition) self.assertEqual(1, len(collection_with_indexing_policy['indexingPolicy']['includedPaths']), 'Unexpected includedPaths length') self.assertEqual(2, len(collection_with_indexing_policy['indexingPolicy']['excludedPaths']), 'Unexpected excluded path count') self.client.DeleteContainer(self.GetDocumentCollectionLink(db, collection_with_indexing_policy, is_name_based)) def test_create_default_indexing_policy_self_link(self): self._test_create_default_indexing_policy(False) def test_create_default_indexing_policy_name_based(self): self._test_create_default_indexing_policy(True) def _test_create_default_indexing_policy(self, is_name_based): # create database db = self.databseForTest # no indexing policy specified collection = self.client.CreateContainer(self.GetDatabaseLink(db, is_name_based), {'id': 'test_create_default_indexing_policy TestCreateDefaultPolicy01' + str(uuid.uuid4())}) self._check_default_indexing_policy_paths(collection['indexingPolicy']) self.client.DeleteContainer(collection['_self']) # partial policy specified collection = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), { 'id': 'test_create_default_indexing_policy TestCreateDefaultPolicy02' + str(uuid.uuid4()), 'indexingPolicy': { 'indexingMode': documents.IndexingMode.Lazy, 'automatic': True } }) self._check_default_indexing_policy_paths(collection['indexingPolicy']) self.client.DeleteContainer(collection['_self']) # default policy collection = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), { 'id': 'test_create_default_indexing_policy TestCreateDefaultPolicy03' + str(uuid.uuid4()), 'indexingPolicy': { } }) self._check_default_indexing_policy_paths(collection['indexingPolicy']) self.client.DeleteContainer(collection['_self']) # missing indexes collection = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), { 'id': 'test_create_default_indexing_policy TestCreateDefaultPolicy04' + str(uuid.uuid4()), 'indexingPolicy': { 'includedPaths': [ { 'path': '/*' } ] } }) self._check_default_indexing_policy_paths(collection['indexingPolicy']) self.client.DeleteContainer(collection['_self']) # missing precision collection = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), { 'id': 'test_create_default_indexing_policy TestCreateDefaultPolicy05' + str(uuid.uuid4()), 'indexingPolicy': { 'includedPaths': [ { 'path': '/*', 'indexes': [ { 'kind': documents.IndexKind.Hash, 'dataType': documents.DataType.String }, { 'kind': documents.IndexKind.Range, 'dataType': documents.DataType.Number } ] } ] } }) self._check_default_indexing_policy_paths(collection['indexingPolicy']) self.client.DeleteContainer(collection['_self']) def test_create_indexing_policy_with_composite_and_spatial_indexes_self_link(self): self._test_create_indexing_policy_with_composite_and_spatial_indexes(False) def test_create_indexing_policy_with_composite_and_spatial_indexes_name_based(self): self._test_create_indexing_policy_with_composite_and_spatial_indexes(True) def _test_create_indexing_policy_with_composite_and_spatial_indexes(self, is_name_based): # create database db = self.databseForTest indexing_policy = { "spatialIndexes": [ { "path": "/path0/*", "types": [ "Point", "LineString", "Polygon" ] }, { "path": "/path1/*", "types": [ "LineString", "Polygon", "MultiPolygon" ] } ], "compositeIndexes": [ [ { "path": "/path1", "order": "ascending" }, { "path": "/path2", "order": "descending" }, { "path": "/path3", "order": "ascending" } ], [ { "path": "/path4", "order": "ascending" }, { "path": "/path5", "order": "descending" }, { "path": "/path6", "order": "ascending" } ] ] } container_id = 'composite_index_spatial_index' + str(uuid.uuid4()) container_definition = {'id': container_id, 'indexingPolicy': indexing_policy} created_container = self.client.CreateContainer(self.GetDatabaseLink(db, is_name_based), container_definition) read_indexing_policy = created_container['indexingPolicy'] self.assertListEqual(indexing_policy['spatialIndexes'], read_indexing_policy['spatialIndexes']) self.assertListEqual(indexing_policy['compositeIndexes'], read_indexing_policy['compositeIndexes']) self.client.DeleteContainer(created_container['_self']) def _check_default_indexing_policy_paths(self, indexing_policy): def __get_first(array): if array: return array[0] else: return None # '/_etag' is present in excluded paths by default self.assertEqual(1, len(indexing_policy['excludedPaths'])) # included paths should be 1: '/'. self.assertEqual(1, len(indexing_policy['includedPaths'])) root_included_path = __get_first([included_path for included_path in indexing_policy['includedPaths'] if included_path['path'] == '/*']) self.assertEqual(0, len(root_included_path['indexes'])) print(root_included_path['indexes']) def test_client_request_timeout(self): connection_policy = documents.ConnectionPolicy() # making timeout 0 ms to make sure it will throw connection_policy.RequestTimeout = 0 with self.assertRaises(Exception): # client does a getDatabaseAccount on initialization, which will time out cosmos_client.CosmosClient(CRUDTests.host, {'masterKey': CRUDTests.masterKey}, connection_policy) def test_query_iterable_functionality(self): def __CreateResources(client): """Creates resources for this test. :Parameters: - `client`: cosmos_client.CosmosClient :Returns: dict """ db = self.databseForTest collection = self.configs.create_single_partition_collection_if_not_exist(self.client) doc1 = client.CreateItem( collection['_self'], { 'id': 'doc1', 'prop1': 'value1'}) doc2 = client.CreateItem( collection['_self'], { 'id': 'doc2', 'prop1': 'value2'}) doc3 = client.CreateItem( collection['_self'], { 'id': 'doc3', 'prop1': 'value3'}) resources = { 'coll': collection, 'doc1': doc1, 'doc2': doc2, 'doc3': doc3 } return resources # Validate QueryIterable by converting it to a list. resources = __CreateResources(self.client) results = self.client.ReadItems(resources['coll']['_self'], {'maxItemCount':2}) docs = list(iter(results)) self.assertEqual(3, len(docs), 'QueryIterable should return all documents' + ' using continuation') self.assertEqual(resources['doc1']['id'], docs[0]['id']) self.assertEqual(resources['doc2']['id'], docs[1]['id']) self.assertEqual(resources['doc3']['id'], docs[2]['id']) # Validate QueryIterable iterator with 'for'. counter = 0 # test QueryIterable with 'for'. for doc in iter(results): counter += 1 if counter == 1: self.assertEqual(resources['doc1']['id'], doc['id'], 'first document should be doc1') elif counter == 2: self.assertEqual(resources['doc2']['id'], doc['id'], 'second document should be doc2') elif counter == 3: self.assertEqual(resources['doc3']['id'], doc['id'], 'third document should be doc3') self.assertEqual(counter, 3) # Get query results page by page. results = self.client.ReadItems(resources['coll']['_self'], {'maxItemCount':2}) first_block = results.fetch_next_block() self.assertEqual(2, len(first_block), 'First block should have 2 entries.') self.assertEqual(resources['doc1']['id'], first_block[0]['id']) self.assertEqual(resources['doc2']['id'], first_block[1]['id']) self.assertEqual(1, len(results.fetch_next_block()), 'Second block should have 1 entry.') self.assertEqual(0, len(results.fetch_next_block()), 'Then its empty.') def test_trigger_functionality_self_link(self): self._test_trigger_functionality(False) def test_trigger_functionality_name_based(self): self._test_trigger_functionality(True) def _test_trigger_functionality(self, is_name_based): triggers_in_collection1 = [ { 'id': 't1', 'body': ( 'function() {' + ' var item = getContext().getRequest().getBody();' + ' item.id = item.id.toUpperCase() + \'t1\';' + ' getContext().getRequest().setBody(item);' + '}'), 'triggerType': documents.TriggerType.Pre, 'triggerOperation': documents.TriggerOperation.All }, { 'id': 'response1', 'body': ( 'function() {' + ' var prebody = getContext().getRequest().getBody();' + ' if (prebody.id != \'TESTING POST TRIGGERt1\')' ' throw \'id mismatch\';' + ' var postbody = getContext().getResponse().getBody();' + ' if (postbody.id != \'TESTING POST TRIGGERt1\')' ' throw \'id mismatch\';' '}'), 'triggerType': documents.TriggerType.Post, 'triggerOperation': documents.TriggerOperation.All }, { 'id': 'response2', # can't be used because setValue is currently disabled 'body': ( 'function() {' + ' var predoc = getContext().getRequest().getBody();' + ' var postdoc = getContext().getResponse().getBody();' + ' getContext().getResponse().setValue(' + ' \'predocname\', predoc.id + \'response2\');' + ' getContext().getResponse().setValue(' + ' \'postdocname\', postdoc.id + \'response2\');' + '}'), 'triggerType': documents.TriggerType.Post, 'triggerOperation': documents.TriggerOperation.All, }] triggers_in_collection2 = [ { 'id': "t2", 'body': "function() { }", # trigger already stringified 'triggerType': documents.TriggerType.Pre, 'triggerOperation': documents.TriggerOperation.All }, { 'id': "t3", 'body': ( 'function() {' + ' var item = getContext().getRequest().getBody();' + ' item.id = item.id.toLowerCase() + \'t3\';' + ' getContext().getRequest().setBody(item);' + '}'), 'triggerType': documents.TriggerType.Pre, 'triggerOperation': documents.TriggerOperation.All }] triggers_in_collection3 = [ { 'id': 'triggerOpType', 'body': 'function() { }', 'triggerType': documents.TriggerType.Post, 'triggerOperation': documents.TriggerOperation.Delete, }] def __CreateTriggers(client, database, collection, triggers, is_name_based): """Creates triggers. :Parameters: - `client`: cosmos_client.CosmosClient - `collection`: dict """ for trigger_i in triggers: trigger = client.CreateTrigger(self.GetDocumentCollectionLink(database, collection, is_name_based), trigger_i) for property in trigger_i: self.assertEqual( trigger[property], trigger_i[property], 'property {property} should match'.format(property=property)) # create database db = self.databseForTest # create collections collection1 = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), { 'id': 'test_trigger_functionality 1 ' + str(uuid.uuid4()) }) collection2 = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), { 'id': 'test_trigger_functionality 2 ' + str(uuid.uuid4()) }) collection3 = self.client.CreateContainer( self.GetDatabaseLink(db, is_name_based), { 'id': 'test_trigger_functionality 3 ' + str(uuid.uuid4()) }) # create triggers __CreateTriggers(self.client, db, collection1, triggers_in_collection1, is_name_based) __CreateTriggers(self.client, db, collection2, triggers_in_collection2, is_name_based) __CreateTriggers(self.client, db, collection3, triggers_in_collection3, is_name_based) # create document triggers_1 = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection1, is_name_based))) self.assertEqual(len(triggers_1), 3) document_1_1 = self.client.CreateItem(self.GetDocumentCollectionLink(db, collection1, is_name_based), { 'id': 'doc1', 'key': 'value' }, { 'preTriggerInclude': 't1' }) self.assertEqual(document_1_1['id'], 'DOC1t1', 'id should be capitalized') document_1_2 = self.client.CreateItem( self.GetDocumentCollectionLink(db, collection1, is_name_based), { 'id': 'testing post trigger' }, { 'postTriggerInclude': 'response1', 'preTriggerInclude': 't1' }) self.assertEqual(document_1_2['id'], 'TESTING POST TRIGGERt1') document_1_3 = self.client.CreateItem(self.GetDocumentCollectionLink(db, collection1, is_name_based), { 'id': 'responseheaders' }, { 'preTriggerInclude': 't1' }) self.assertEqual(document_1_3['id'], "RESPONSEHEADERSt1") triggers_2 = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection2, is_name_based))) self.assertEqual(len(triggers_2), 2) document_2_1 = self.client.CreateItem(self.GetDocumentCollectionLink(db, collection2, is_name_based), { 'id': 'doc2', 'key2': 'value2' }, { 'preTriggerInclude': 't2' }) self.assertEqual(document_2_1['id'], 'doc2', 'id shouldn\'t change') document_2_2 = self.client.CreateItem(self.GetDocumentCollectionLink(db, collection2, is_name_based), { 'id': 'Doc3', 'prop': 'empty' }, { 'preTriggerInclude': 't3' }) self.assertEqual(document_2_2['id'], 'doc3t3') triggers_3 = list(self.client.ReadTriggers(self.GetDocumentCollectionLink(db, collection3, is_name_based))) self.assertEqual(len(triggers_3), 1) with self.assertRaises(Exception): self.client.CreateItem(self.GetDocumentCollectionLink(db, collection3, is_name_based), { 'id': 'Docoptype' }, { 'postTriggerInclude': 'triggerOpType' }) self.client.DeleteContainer(collection1['_self']) self.client.DeleteContainer(collection2['_self']) self.client.DeleteContainer(collection3['_self']) def test_stored_procedure_functionality_self_link(self): self._test_stored_procedure_functionality(False) def test_stored_procedure_functionality_name_based(self): self._test_stored_procedure_functionality(True) def _test_stored_procedure_functionality(self, is_name_based): # create database db = self.databseForTest # create collection collection = self.configs.create_single_partition_collection_if_not_exist(self.client) sproc1 = { 'id': 'storedProcedure1' + str(uuid.uuid4()), 'body': ( 'function () {' + ' for (var i = 0; i < 1000; i++) {' + ' var item = getContext().getResponse().getBody();' + ' if (i > 0 && item != i - 1) throw \'body mismatch\';' + ' getContext().getResponse().setBody(i);' + ' }' + '}') } retrieved_sproc = self.client.CreateStoredProcedure(self.GetDocumentCollectionLink(db, collection, is_name_based), sproc1) result = self.client.ExecuteStoredProcedure(self.GetStoredProcedureLink(db, collection, retrieved_sproc, is_name_based), None) self.assertEqual(result, 999) sproc2 = { 'id': 'storedProcedure2' + str(uuid.uuid4()), 'body': ( 'function () {' + ' for (var i = 0; i < 10; i++) {' + ' getContext().getResponse().appendValue(\'Body\', i);' + ' }' + '}') } retrieved_sproc2 = self.client.CreateStoredProcedure(self.GetDocumentCollectionLink(db, collection, is_name_based), sproc2) result = self.client.ExecuteStoredProcedure(self.GetStoredProcedureLink(db, collection, retrieved_sproc2, is_name_based), None) self.assertEqual(int(result), 123456789) sproc3 = { 'id': 'storedProcedure3' + str(uuid.uuid4()), 'body': ( 'function (input) {' + ' getContext().getResponse().setBody(' + ' \'a\' + input.temp);' + '}') } retrieved_sproc3 = self.client.CreateStoredProcedure(self.GetDocumentCollectionLink(db, collection, is_name_based), sproc3) result = self.client.ExecuteStoredProcedure(self.GetStoredProcedureLink(db, collection, retrieved_sproc3, is_name_based), {'temp': 'so'}) self.assertEqual(result, 'aso') def __ValidateOfferResponseBody(self, offer, expected_coll_link, expected_offer_type): self.assert_(offer.get('id'), 'Id cannot be null.') self.assert_(offer.get('_rid'), 'Resource Id (Rid) cannot be null.') self.assert_(offer.get('_self'), 'Self Link cannot be null.') self.assert_(offer.get('resource'), 'Resource Link cannot be null.') self.assertTrue(offer['_self'].find(offer['id']) != -1, 'Offer id not contained in offer self link.') self.assertEqual(expected_coll_link.strip('/'), offer['resource'].strip('/')) if (expected_offer_type): self.assertEqual(expected_offer_type, offer.get('offerType')) def test_offer_read_and_query(self): # Create database. db = self.databseForTest offers = list(self.client.ReadOffers()) initial_count = len(offers) # Create collection. collection = self.client.CreateContainer(db['_self'], { 'id': 'test_offer_read_and_query ' + str(uuid.uuid4()) }) offers = list(self.client.ReadOffers()) self.assertEqual(initial_count+1, len(offers)) offers = self.GetCollectionOffers(self.client, collection['_rid']) self.assertEqual(1, len(offers)) expected_offer = offers[0] self.__ValidateOfferResponseBody(expected_offer, collection.get('_self'), None) # Read the offer. read_offer = self.client.ReadOffer(expected_offer.get('_self')) self.__ValidateOfferResponseBody(read_offer, collection.get('_self'), expected_offer.get('offerType')) # Check if the read resource is what we expected. self.assertEqual(expected_offer.get('id'), read_offer.get('id')) self.assertEqual(expected_offer.get('_rid'), read_offer.get('_rid')) self.assertEqual(expected_offer.get('_self'), read_offer.get('_self')) self.assertEqual(expected_offer.get('resource'), read_offer.get('resource')) # Query for the offer. offers = list(self.client.QueryOffers( { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name': '@id', 'value': expected_offer['id']} ] })) self.assertEqual(1, len(offers)) query_one_offer = offers[0] self.__ValidateOfferResponseBody(query_one_offer, collection.get('_self'), expected_offer.get('offerType')) # Check if the query result is what we expected. self.assertEqual(expected_offer.get('id'), query_one_offer.get('id')) self.assertEqual(expected_offer.get('_rid'), query_one_offer.get('_rid')) self.assertEqual(expected_offer.get('_self'), query_one_offer.get('_self')) self.assertEqual(expected_offer.get('resource'), query_one_offer.get('resource')) # Expects an exception when reading offer with bad offer link. self.__AssertHTTPFailureWithStatus(StatusCodes.BAD_REQUEST, self.client.ReadOffer, expected_offer.get('_self')[:-1] + 'x') # Now delete the collection. self.client.DeleteContainer(collection.get('_self')) # Reading fails. self.__AssertHTTPFailureWithStatus(StatusCodes.NOT_FOUND, self.client.ReadOffer, expected_offer.get('_self')) # Read feed now returns 0 results. offers = list(self.client.ReadOffers()) self.assertEqual(initial_count, len(offers)) def test_offer_replace(self): # Create database. db = self.databseForTest # Create collection. collection = self.configs.create_single_partition_collection_if_not_exist(self.client) offers = self.GetCollectionOffers(self.client, collection['_rid']) self.assertEqual(1, len(offers)) expected_offer = offers[0] self.__ValidateOfferResponseBody(expected_offer, collection.get('_self'), None) # Replace the offer. offer_to_replace = dict(expected_offer) offer_to_replace['content']['offerThroughput'] += 100 replaced_offer = self.client.ReplaceOffer(offer_to_replace['_self'], offer_to_replace) self.__ValidateOfferResponseBody(replaced_offer, collection.get('_self'), None) # Check if the replaced offer is what we expect. self.assertEqual(offer_to_replace.get('id'), replaced_offer.get('id')) self.assertEqual(offer_to_replace.get('_rid'), replaced_offer.get('_rid')) self.assertEqual(offer_to_replace.get('_self'), replaced_offer.get('_self')) self.assertEqual(offer_to_replace.get('resource'), replaced_offer.get('resource')) self.assertEqual(offer_to_replace.get('content').get('offerThroughput'), replaced_offer.get('content').get('offerThroughput')) # Expects an exception when replacing an offer with bad id. offer_to_replace_bad_id = dict(offer_to_replace) offer_to_replace_bad_id['_rid'] = 'NotAllowed' self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.ReplaceOffer, offer_to_replace_bad_id['_self'], offer_to_replace_bad_id) # Expects an exception when replacing an offer with bad rid. offer_to_replace_bad_rid = dict(offer_to_replace) offer_to_replace_bad_rid['_rid'] = 'InvalidRid' self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.ReplaceOffer, offer_to_replace_bad_rid['_self'], offer_to_replace_bad_rid) # Expects an exception when replaceing an offer with null id and rid. offer_to_replace_null_ids = dict(offer_to_replace) offer_to_replace_null_ids['id'] = None offer_to_replace_null_ids['_rid'] = None self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.ReplaceOffer, offer_to_replace_null_ids['_self'], offer_to_replace_null_ids) def test_collection_with_offer_type(self): # create database created_db = self.databseForTest # create a collection offers = list(self.client.ReadOffers()) before_offers_count = len(offers) collection_definition = { 'id': 'test_collection_with_offer_type ' + str(uuid.uuid4()) } collection = self.client.CreateContainer(created_db['_self'], collection_definition, { 'offerType': 'S2' }) offers = list(self.client.ReadOffers()) self.assertEqual(before_offers_count + 1, len(offers)) offers = self.GetCollectionOffers(self.client, collection['_rid']) self.assertEqual(1, len(offers)) expected_offer = offers[0] # We should have an offer of type S2. self.__ValidateOfferResponseBody(expected_offer, collection.get('_self'), 'S2') self.client.DeleteContainer(collection['_self']) def test_database_account_functionality(self): # Validate database account functionality. database_account = self.client.GetDatabaseAccount() self.assertEqual(database_account.DatabasesLink, '/dbs/') self.assertEqual(database_account.MediaLink, '/media/') if (HttpHeaders.MaxMediaStorageUsageInMB in self.client.last_response_headers): self.assertEqual( database_account.MaxMediaStorageUsageInMB, self.client.last_response_headers[ HttpHeaders.MaxMediaStorageUsageInMB]) if (HttpHeaders.CurrentMediaStorageUsageInMB in self.client.last_response_headers): self.assertEqual( database_account.CurrentMediaStorageUsageInMB, self.client.last_response_headers[ HttpHeaders. CurrentMediaStorageUsageInMB]) self.assertTrue( database_account.ConsistencyPolicy['defaultConsistencyLevel'] != None) def test_index_progress_headers_self_link(self): self._test_index_progress_headers(False) def test_index_progress_headers_name_based(self): self._test_index_progress_headers(True) def _test_index_progress_headers(self, is_name_based): created_db = self.databseForTest consistent_coll = self.client.CreateContainer(self.GetDatabaseLink(created_db, is_name_based), { 'id': 'test_index_progress_headers consistent_coll ' + str(uuid.uuid4()) }) self.client.ReadContainer(self.GetDocumentCollectionLink(created_db, consistent_coll, is_name_based)) self.assertFalse(HttpHeaders.LazyIndexingProgress in self.client.last_response_headers) self.assertTrue(HttpHeaders.IndexTransformationProgress in self.client.last_response_headers) lazy_coll = self.client.CreateContainer(self.GetDatabaseLink(created_db, is_name_based), { 'id': 'test_index_progress_headers lazy_coll ' + str(uuid.uuid4()), 'indexingPolicy': { 'indexingMode' : documents.IndexingMode.Lazy } }) self.client.ReadContainer(self.GetDocumentCollectionLink(created_db, lazy_coll, is_name_based)) self.assertTrue(HttpHeaders.LazyIndexingProgress in self.client.last_response_headers) self.assertTrue(HttpHeaders.IndexTransformationProgress in self.client.last_response_headers) none_coll = self.client.CreateContainer(self.GetDatabaseLink(created_db, is_name_based), { 'id': 'test_index_progress_headers none_coll ' + str(uuid.uuid4()), 'indexingPolicy': { 'indexingMode': documents.IndexingMode.NoIndex, 'automatic': False } }) self.client.ReadContainer(self.GetDocumentCollectionLink(created_db, none_coll, is_name_based)) self.assertFalse(HttpHeaders.LazyIndexingProgress in self.client.last_response_headers) self.assertTrue(HttpHeaders.IndexTransformationProgress in self.client.last_response_headers) self.client.DeleteContainer(consistent_coll['_self']) self.client.DeleteContainer(lazy_coll['_self']) self.client.DeleteContainer(none_coll['_self']) # To run this test, please provide your own CA certs file or download one from # http://curl.haxx.se/docs/caextract.html # # def test_ssl_connection(self): # connection_policy = documents.ConnectionPolicy() # connection_policy.SSLConfiguration = documents.SSLConfiguration() # connection_policy.SSLConfiguration.SSLCaCerts = './cacert.pem' # client = cosmos_client.CosmosClient(CRUDTests.host, {'masterKey': CRUDTests.masterKey}, connection_policy) # # Read databases after creation. # databases = list(client.ReadDatabases()) def test_id_validation(self): # Id shouldn't end with space. database_definition = { 'id': 'id_with_space ' } try: self.client.CreateDatabase(database_definition) self.assertFalse(True) except ValueError as e: self.assertEqual('Id ends with a space.', e.args[0]) # Id shouldn't contain '/'. database_definition = { 'id': 'id_with_illegal/_char' } try: self.client.CreateDatabase(database_definition) self.assertFalse(True) except ValueError as e: self.assertEqual('Id contains illegal chars.', e.args[0]) # Id shouldn't contain '\\'. database_definition = { 'id': 'id_with_illegal\\_char' } try: self.client.CreateDatabase(database_definition) self.assertFalse(True) except ValueError as e: self.assertEqual('Id contains illegal chars.', e.args[0]) # Id shouldn't contain '?'. database_definition = { 'id': 'id_with_illegal?_char' } try: self.client.CreateDatabase(database_definition) self.assertFalse(True) except ValueError as e: self.assertEqual('Id contains illegal chars.', e.args[0]) # Id shouldn't contain '#'. database_definition = { 'id': 'id_with_illegal#_char' } try: self.client.CreateDatabase(database_definition) self.assertFalse(True) except ValueError as e: self.assertEqual('Id contains illegal chars.', e.args[0]) # Id can begin with space database_definition = { 'id': ' id_begin_space' } db = self.client.CreateDatabase(database_definition) self.assertTrue(True) self.client.DeleteDatabase(db['_self']) def test_id_case_validation(self): # create database created_db = self.databseForTest uuid_string = str(uuid.uuid4()) # pascalCase collection_definition1 = { 'id': 'sampleCollection ' + uuid_string } # CamelCase collection_definition2 = { 'id': 'SampleCollection ' + uuid_string } # Verify that no collections exist collections = list(self.client.ReadContainers(self.GetDatabaseLink(created_db, True))) number_of_existing_collections = len(collections) # create 2 collections with different casing of IDs created_collection1 = self.client.CreateContainer(self.GetDatabaseLink(created_db, True), collection_definition1) created_collection2 = self.client.CreateContainer(self.GetDatabaseLink(created_db, True), collection_definition2) collections = list(self.client.ReadContainers(self.GetDatabaseLink(created_db, True))) # verify if a total of 2 collections got created self.assertEqual(len(collections), number_of_existing_collections + 2) # verify that collections are created with specified IDs self.assertEqual(collection_definition1['id'], created_collection1['id']) self.assertEqual(collection_definition2['id'], created_collection2['id']) self.client.DeleteContainer(created_collection1['_self']) self.client.DeleteContainer(created_collection2['_self']) def test_id_unicode_validation(self): # create database created_db = self.databseForTest # unicode chars in Hindi for Id which translates to: "Hindi is the national language of India" collection_definition1 = { 'id': u'हिन्दी भारत की राष्ट्रीय भाषा है' } # Special chars for Id collection_definition2 = { 'id': "!@$%^&*()-~`'_[]{}|;:,.<>" } # verify that collections are created with specified IDs created_collection1 = self.client.CreateContainer(self.GetDatabaseLink(created_db, True), collection_definition1) created_collection2 = self.client.CreateContainer(self.GetDatabaseLink(created_db, True), collection_definition2) self.assertEqual(collection_definition1['id'], created_collection1['id']) self.assertEqual(collection_definition2['id'], created_collection2['id']) self.client.DeleteContainer(created_collection1['_self']) self.client.DeleteContainer(created_collection2['_self']) def GetDatabaseLink(self, database, is_name_based=True): if is_name_based: return 'dbs/' + database['id'] else: return database['_self'] def GetUserLink(self, database, user, is_name_based=True): if is_name_based: return self.GetDatabaseLink(database) + '/users/' + user['id'] else: return user['_self'] def GetPermissionLink(self, database, user, permission, is_name_based=True): if is_name_based: return self.GetUserLink(database, user) + '/permissions/' + permission['id'] else: return permission['_self'] def GetDocumentCollectionLink(self, database, document_collection, is_name_based=True): if is_name_based: return self.GetDatabaseLink(database) + '/colls/' + document_collection['id'] else: return document_collection['_self'] def GetDocumentLink(self, database, document_collection, document, is_name_based=True): if is_name_based: return self.GetDocumentCollectionLink(database, document_collection) + '/docs/' + document['id'] else: return document['_self'] def GetAttachmentLink(self, database, document_collection, document, attachment, is_name_based=True): if is_name_based: return self.GetDocumentLink(database, document_collection, document) + '/attachments/' + attachment['id'] else: return attachment['_self'] def GetTriggerLink(self, database, document_collection, trigger, is_name_based=True): if is_name_based: return self.GetDocumentCollectionLink(database, document_collection) + '/triggers/' + trigger['id'] else: return trigger['_self'] def GetUserDefinedFunctionLink(self, database, document_collection, user_defined_function, is_name_based=True): if is_name_based: return self.GetDocumentCollectionLink(database, document_collection) + '/udfs/' + user_defined_function['id'] else: return user_defined_function['_self'] def GetStoredProcedureLink(self, database, document_collection, stored_procedure, is_name_based=True): if is_name_based: return self.GetDocumentCollectionLink(database, document_collection) + '/sprocs/' + stored_procedure['id'] else: return stored_procedure['_self'] def GetConflictLink(self, database, document_collection, conflict, is_name_based=True): if is_name_based: return self.GetDocumentCollectionLink(database, document_collection) + '/conflicts/' + conflict['id'] else: return conflict['_self'] def GetCollectionOffers(self, client, collection_rid): return list(client.QueryOffers( { 'query': 'SELECT * FROM root r WHERE r.offerResourceId=@offerResourceId', 'parameters': [ { 'name': '@offerResourceId', 'value': collection_rid} ] })) if __name__ == '__main__': try: unittest.main() except SystemExit as inst: if inst.args[0] is True: # raised by sys.exit(True) when tests failed raise azure-cosmos-python-3.1.1/test/encoding_tests.py000066400000000000000000000050201352206500100220120ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unittest import uuid import pytest import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents import test.test_config as test_config @pytest.mark.usefixtures("teardown") class EncodingTest(unittest.TestCase): """Test to ensure escaping of non-ascii characters from partition key""" host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy client = cosmos_client.CosmosClient(host, {'masterKey': masterKey}, connectionPolicy) created_collection = test_config._test_config.create_multi_partition_collection_with_custom_pk_if_not_exist(client) def test_unicode_characters_in_partition_key (self): test_string = u'€€ کلید پارتیشن विभाजन कुंजी 123' document_definition = {'pk': test_string, 'id':'myid' + str(uuid.uuid4())} created_doc = self.client.CreateItem(self.created_collection['_self'], document_definition) read_options = {'partitionKey': test_string } read_doc = self.client.ReadItem(created_doc['_self'], read_options) self.assertEqual(read_doc['pk'], test_string) def test_create_document_with_line_separator_para_seperator_next_line_unicodes (self): test_string = u'Line Separator (
) & Paragraph Separator (
) & Next Line (…) & نیم‌فاصله' document_definition = {'pk': 'pk', 'id':'myid' + str(uuid.uuid4()), 'unicode_content':test_string } created_doc = self.client.CreateItem(self.created_collection['_self'], document_definition) read_options = {'partitionKey': 'pk' } read_doc = self.client.ReadItem(created_doc['_self'], read_options) self.assertEqual(read_doc['unicode_content'], test_string) def test_create_stored_procedure_with_line_separator_para_seperator_next_line_unicodes (self): test_string = 'Line Separator (
) & Paragraph Separator (
) & Next Line (…) & نیم‌فاصله' test_string_unicode = u'Line Separator (
) & Paragraph Separator (
) & Next Line (…) & نیم‌فاصله' stored_proc_definition = {'id':'myid' + str(uuid.uuid4()), 'body': test_string} created_sp = self.client.CreateStoredProcedure(self.created_collection['_self'], stored_proc_definition) read_sp = self.client.ReadStoredProcedure(created_sp['_self'], dict()) self.assertEqual(read_sp['body'], test_string_unicode) if __name__ == '__main__': unittest.main() azure-cosmos-python-3.1.1/test/env_test.py000066400000000000000000000103471352206500100206410ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2019 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import uuid import pytest import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client import test.test_config as test_config import os #IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos account. # Collections are billing entities. By running these test cases, you may incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class EnvTest(unittest.TestCase): """Env Tests. """ host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy @classmethod def setUpClass(cls): # creates the database, collection, and insert all the documents # we will gain some speed up in running the tests by creating the database, collection and inserting all the docs only once if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_ENDPOINT_HERE]'): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") os.environ["COSMOS_ENDPOINT"] = cls.host os.environ["COSMOS_KEY"] = cls.masterKey cls.client = cosmos_client.CosmosClient(None, None, cls.connectionPolicy) cls.created_db = test_config._test_config.create_database_if_not_exist(cls.client) cls.created_collection = test_config._test_config.create_single_partition_collection_if_not_exist(cls.client) cls.collection_link = cls.GetDocumentCollectionLink(cls.created_db, cls.created_collection) @classmethod def tearDownClass(cls): del os.environ['COSMOS_ENDPOINT'] del os.environ['COSMOS_KEY'] def test_insert(self): # create a document using the document definition d = {'id': '1', 'name': 'sample document', 'spam': 'eggs', 'cnt': '1', 'key': 'value', 'spam2': 'eggs', } self.client.CreateItem(self.collection_link, d) @classmethod def GetDatabaseLink(cls, database, is_name_based=True): if is_name_based: return 'dbs/' + database['id'] else: return database['_self'] @classmethod def GetDocumentCollectionLink(cls, database, document_collection, is_name_based=True): if is_name_based: return cls.GetDatabaseLink(database) + '/colls/' + document_collection['id'] else: return document_collection['_self'] @classmethod def GetDocumentLink(cls, database, document_collection, document, is_name_based=True): if is_name_based: return cls.GetDocumentCollectionLink(database, document_collection) + '/docs/' + document['id'] else: return document['_self'] if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()azure-cosmos-python-3.1.1/test/globaldb_mock_tests.py000066400000000000000000000213561352206500100230150ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import json import pytest import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents import azure.cosmos.errors as errors import azure.cosmos.constants as constants from azure.cosmos.http_constants import StatusCodes import azure.cosmos.global_endpoint_manager as global_endpoint_manager import azure.cosmos.retry_utility as retry_utility import test.test_config as test_config location_changed = False class MockGlobalEndpointManager: def __init__(self, client): self.Client = client self.DefaultEndpoint = client.url_connection self._ReadEndpoint = client.url_connection self._WriteEndpoint = client.url_connection self.EnableEndpointDiscovery = client.connection_policy.EnableEndpointDiscovery self.IsEndpointCacheInitialized = False self.refresh_count = 0 self.DatabaseAccountAvailable = True def RefreshEndpointList(self): global location_changed if not location_changed: database_account = self.GetDatabaseAccount1() else: database_account = self.GetDatabaseAccount2() if self.DatabaseAccountAvailable is False: database_account = None writable_locations = [] readable_locations = [] else: writable_locations = database_account.WritableLocations readable_locations = database_account.ReadableLocations self._WriteEndpoint, self._ReadEndpoint = self.UpdateLocationsCache(writable_locations, readable_locations) @property def ReadEndpoint(self): if not self.IsEndpointCacheInitialized: self.RefreshEndpointList() return self._ReadEndpoint @property def WriteEndpoint(self): if not self.IsEndpointCacheInitialized: self.RefreshEndpointList() return self._WriteEndpoint def GetDatabaseAccount1(self): database_account = documents.DatabaseAccount() database_account._ReadableLocations = [{'name' : Test_globaldb_mock_tests.read_location, 'databaseAccountEndpoint' : Test_globaldb_mock_tests.read_location_host}] database_account._WritableLocations = [{'name' : Test_globaldb_mock_tests.write_location, 'databaseAccountEndpoint' : Test_globaldb_mock_tests.write_location_host}] return database_account def GetDatabaseAccount2(self): database_account = documents.DatabaseAccount() database_account._ReadableLocations = [{'name' : Test_globaldb_mock_tests.write_location, 'databaseAccountEndpoint' : Test_globaldb_mock_tests.write_location_host}] database_account._WritableLocations = [{'name' : Test_globaldb_mock_tests.read_location, 'databaseAccountEndpoint' : Test_globaldb_mock_tests.read_location_host}] return database_account def UpdateLocationsCache(self, writable_locations, readable_locations): if len(writable_locations) == 0: write_endpoint = self.DefaultEndpoint else: write_endpoint = writable_locations[0][constants._Constants.DatabaseAccountEndpoint] if len(readable_locations) == 0: read_endpoint = write_endpoint else: read_endpoint = writable_locations[0][constants._Constants.DatabaseAccountEndpoint] return write_endpoint, read_endpoint @pytest.mark.usefixtures("teardown") class Test_globaldb_mock_tests(unittest.TestCase): host = test_config._test_config.global_host write_location_host = test_config._test_config.write_location_host read_location_host = test_config._test_config.read_location_host masterKey = test_config._test_config.global_masterKey write_location = test_config._test_config.write_location read_location = test_config._test_config.read_location @classmethod def setUpClass(cls): if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_GLOBAL_ENDPOINT_HERE]'): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") def setUp(self): self.endpoint_discovery_retry_count = 0 # Copying the original objects and functions before assigning the mock versions of them self.OriginalGetDatabaseAccountStub = global_endpoint_manager._GlobalEndpointManager._GetDatabaseAccountStub self.OriginalGlobalEndpointManager = global_endpoint_manager._GlobalEndpointManager self.OriginalExecuteFunction = retry_utility._ExecuteFunction # Make azure-cosmos use the MockGlobalEndpointManager global_endpoint_manager._GlobalEndpointManager = MockGlobalEndpointManager def tearDown(self): # Restoring the original objects and functions global_endpoint_manager._GlobalEndpointManager = self.OriginalGlobalEndpointManager global_endpoint_manager._GlobalEndpointManager._GetDatabaseAccountStub = self.OriginalGetDatabaseAccountStub retry_utility._ExecuteFunction = self.OriginalExecuteFunction def MockExecuteFunction(self, function, *args, **kwargs): global location_changed if self.endpoint_discovery_retry_count == 2: retry_utility._ExecuteFunction = self.OriginalExecuteFunction return (json.dumps([{ 'id': 'mock database' }]), None) else: self.endpoint_discovery_retry_count += 1 location_changed = True raise errors.HTTPFailure(StatusCodes.FORBIDDEN, "Forbidden", {'x-ms-substatus' : 3}) def MockGetDatabaseAccountStub(self, endpoint): raise errors.HTTPFailure(StatusCodes.SERVICE_UNAVAILABLE, "Service unavailable") def MockCreateDatabase(self, client, database): self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self.MockExecuteFunction client.CreateDatabase(database) def test_globaldb_endpoint_discovery_retry_policy(self): connection_policy = documents.ConnectionPolicy() connection_policy.EnableEndpointDiscovery = True write_location_client = cosmos_client.CosmosClient(Test_globaldb_mock_tests.write_location_host, {'masterKey': Test_globaldb_mock_tests.masterKey}, connection_policy) self.assertEqual(write_location_client._global_endpoint_manager.WriteEndpoint, Test_globaldb_mock_tests.write_location_host) self.MockCreateDatabase(write_location_client, { 'id': 'mock database' }) self.assertEqual(write_location_client._global_endpoint_manager.WriteEndpoint, Test_globaldb_mock_tests.read_location_host) def test_globaldb_database_account_unavailable(self): connection_policy = documents.ConnectionPolicy() connection_policy.EnableEndpointDiscovery = True client = cosmos_client.CosmosClient(Test_globaldb_mock_tests.host, {'masterKey': Test_globaldb_mock_tests.masterKey}, connection_policy) self.assertEqual(client._global_endpoint_manager.WriteEndpoint, Test_globaldb_mock_tests.write_location_host) self.assertEqual(client._global_endpoint_manager.ReadEndpoint, Test_globaldb_mock_tests.write_location_host) global_endpoint_manager._GlobalEndpointManager._GetDatabaseAccountStub = self.MockGetDatabaseAccountStub client._global_endpoint_manager.DatabaseAccountAvailable = False client._global_endpoint_manager.RefreshEndpointList() self.assertEqual(client._global_endpoint_manager.WriteEndpoint, Test_globaldb_mock_tests.host) self.assertEqual(client._global_endpoint_manager.ReadEndpoint, Test_globaldb_mock_tests.host) if __name__ == '__main__': unittest.main() azure-cosmos-python-3.1.1/test/globaldb_tests.py000066400000000000000000000530301352206500100217760ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. from six.moves.urllib.parse import urlparse import six import unittest import time import pytest import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents import azure.cosmos.errors as errors import azure.cosmos.global_endpoint_manager as global_endpoint_manager import azure.cosmos.endpoint_discovery_retry_policy as endpoint_discovery_retry_policy import azure.cosmos.retry_utility as retry_utility from azure.cosmos.http_constants import HttpHeaders, StatusCodes, SubStatusCodes import test.test_config as test_config #IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos account. # Collections are billing entities. By running these test cases, you may incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class Test_globaldb_tests(unittest.TestCase): host = test_config._test_config.global_host write_location_host = test_config._test_config.write_location_host read_location_host = test_config._test_config.read_location_host read_location2_host = test_config._test_config.read_location2_host masterKey = test_config._test_config.global_masterKey write_location = test_config._test_config.write_location read_location = test_config._test_config.read_location read_location2 = test_config._test_config.read_location2 test_database_id = 'testdb' test_collection_id = 'testcoll' def __AssertHTTPFailureWithStatus(self, status_code, sub_status, func, *args, **kwargs): """Assert HTTP failure with status. :Parameters: - `status_code`: int - `sub_status`: int - `func`: function """ try: func(*args, **kwargs) self.assertFalse(True, 'function should fail.') except errors.HTTPFailure as inst: self.assertEqual(inst.status_code, status_code) self.assertEqual(inst.sub_status, sub_status) @classmethod def setUpClass(cls): if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_GLOBAL_ENDPOINT_HERE]'): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") def setUp(self): self.client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}) # Create the test database only when it's not already present query_iterable = self.client.QueryDatabases('SELECT * FROM root r WHERE r.id=\'' + Test_globaldb_tests.test_database_id + '\'') it = iter(query_iterable) self.test_db = next(it, None) if self.test_db is None: self.test_db = self.client.CreateDatabase({'id' : Test_globaldb_tests.test_database_id}) # Create the test collection only when it's not already present query_iterable = self.client.QueryContainers(self.test_db['_self'], 'SELECT * FROM root r WHERE r.id=\'' + Test_globaldb_tests.test_collection_id + '\'') it = iter(query_iterable) self.test_coll = next(it, None) if self.test_coll is None: self.test_coll = self.client.CreateContainer(self.test_db['_self'], {'id' : Test_globaldb_tests.test_collection_id}) def tearDown(self): # Delete all the documents created by the test case for clean up purposes docs = list(self.client.ReadItems(self.test_coll['_self'])) for doc in docs: self.client.DeleteItem(doc['_self']) def test_globaldb_read_write_endpoints(self): connection_policy = documents.ConnectionPolicy() connection_policy.EnableEndpointDiscovery = False client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} # When EnableEndpointDiscovery is False, WriteEndpoint is set to the endpoint passed while creating the client instance created_document = client.CreateItem(self.test_coll['_self'], document_definition) self.assertEqual(client.WriteEndpoint, Test_globaldb_tests.host) # Delay to get these resources replicated to read location due to Eventual consistency time.sleep(5) client.ReadItem(created_document['_self']) content_location = str(client.last_response_headers[HttpHeaders.ContentLocation]) content_location_url = urlparse(content_location) host_url = urlparse(Test_globaldb_tests.host) # When EnableEndpointDiscovery is False, ReadEndpoint is set to the endpoint passed while creating the client instance self.assertEqual(str(content_location_url.hostname), str(host_url.hostname)) self.assertEqual(client.ReadEndpoint, Test_globaldb_tests.host) connection_policy.EnableEndpointDiscovery = True document_definition['id'] = 'doc2' client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) # When EnableEndpointDiscovery is True, WriteEndpoint is set to the write endpoint created_document = client.CreateItem(self.test_coll['_self'], document_definition) self.assertEqual(client.WriteEndpoint, Test_globaldb_tests.write_location_host) # Delay to get these resources replicated to read location due to Eventual consistency time.sleep(5) client.ReadItem(created_document['_self']) content_location = str(client.last_response_headers[HttpHeaders.ContentLocation]) content_location_url = urlparse(content_location) write_location_url = urlparse(Test_globaldb_tests.write_location_host) # If no preferred locations is set, we return the write endpoint as ReadEndpoint for better latency performance self.assertEqual(str(content_location_url.hostname), str(write_location_url.hostname)) self.assertEqual(client.ReadEndpoint, Test_globaldb_tests.write_location_host) def test_globaldb_endpoint_discovery(self): connection_policy = documents.ConnectionPolicy() connection_policy.EnableEndpointDiscovery = False read_location_client = cosmos_client.CosmosClient(Test_globaldb_tests.read_location_host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} # Create Document will fail for the read location client since it has EnableEndpointDiscovery set to false, and hence the request will directly go to # the endpoint that was used to create the client instance(which happens to be a read endpoint) self.__AssertHTTPFailureWithStatus( StatusCodes.FORBIDDEN, SubStatusCodes.WRITE_FORBIDDEN, read_location_client.CreateItem, self.test_coll['_self'], document_definition) # Query databases will pass for the read location client as it's a GET operation list(read_location_client.QueryDatabases({ 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name':'@id', 'value': self.test_db['id'] } ] })) connection_policy.EnableEndpointDiscovery = True read_location_client = cosmos_client.CosmosClient(Test_globaldb_tests.read_location_host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) # CreateDocument call will go to the WriteEndpoint as EnableEndpointDiscovery is set to True and client will resolve the right endpoint based on the operation created_document = read_location_client.CreateItem(self.test_coll['_self'], document_definition) self.assertEqual(created_document['id'], document_definition['id']) def test_globaldb_preferred_locations(self): connection_policy = documents.ConnectionPolicy() connection_policy.EnableEndpointDiscovery = True client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} created_document = client.CreateItem(self.test_coll['_self'], document_definition) self.assertEqual(created_document['id'], document_definition['id']) # Delay to get these resources replicated to read location due to Eventual consistency time.sleep(5) client.ReadItem(created_document['_self']) content_location = str(client.last_response_headers[HttpHeaders.ContentLocation]) content_location_url = urlparse(content_location) write_location_url = urlparse(Test_globaldb_tests.write_location_host) # If no preferred locations is set, we return the write endpoint as ReadEndpoint for better latency performance self.assertEqual(str(content_location_url.hostname), str(write_location_url.hostname)) self.assertEqual(client.ReadEndpoint, Test_globaldb_tests.write_location_host) connection_policy.PreferredLocations = [Test_globaldb_tests.read_location2] client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) document_definition['id'] = 'doc2' created_document = client.CreateItem(self.test_coll['_self'], document_definition) # Delay to get these resources replicated to read location due to Eventual consistency time.sleep(5) client.ReadItem(created_document['_self']) content_location = str(client.last_response_headers[HttpHeaders.ContentLocation]) content_location_url = urlparse(content_location) read_location2_url = urlparse(Test_globaldb_tests.read_location2_host) # Test that the preferred location is set as ReadEndpoint instead of default write endpoint when no preference is set self.assertEqual(str(content_location_url.hostname), str(read_location2_url.hostname)) self.assertEqual(client.ReadEndpoint, Test_globaldb_tests.read_location2_host) def test_globaldb_endpoint_assignments(self): connection_policy = documents.ConnectionPolicy() connection_policy.EnableEndpointDiscovery = False client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) # When EnableEndpointDiscovery is set to False, both Read and Write Endpoints point to endpoint passed while creating the client instance self.assertEqual(client._global_endpoint_manager.WriteEndpoint, Test_globaldb_tests.host) self.assertEqual(client._global_endpoint_manager.ReadEndpoint, Test_globaldb_tests.host) connection_policy.EnableEndpointDiscovery = True client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) # If no preferred locations is set, we return the write endpoint as ReadEndpoint for better latency performance, write endpoint is set as expected self.assertEqual(client._global_endpoint_manager.WriteEndpoint, Test_globaldb_tests.write_location_host) self.assertEqual(client._global_endpoint_manager.ReadEndpoint, Test_globaldb_tests.write_location_host) connection_policy.PreferredLocations = [Test_globaldb_tests.read_location2] client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) # Test that the preferred location is set as ReadEndpoint instead of default write endpoint when no preference is set self.assertEqual(client._global_endpoint_manager.WriteEndpoint, Test_globaldb_tests.write_location_host) self.assertEqual(client._global_endpoint_manager.ReadEndpoint, Test_globaldb_tests.read_location2_host) def test_globaldb_update_locations_cache(self): client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}) writable_locations = [{'name' : Test_globaldb_tests.write_location, 'databaseAccountEndpoint' : Test_globaldb_tests.write_location_host}] readable_locations = [{'name' : Test_globaldb_tests.read_location, 'databaseAccountEndpoint' : Test_globaldb_tests.read_location_host}, {'name' : Test_globaldb_tests.read_location2, 'databaseAccountEndpoint' : Test_globaldb_tests.read_location2_host}] write_endpoint, read_endpoint = client._global_endpoint_manager.UpdateLocationsCache(writable_locations, readable_locations) # If no preferred locations is set, we return the write endpoint as ReadEndpoint for better latency performance, write endpoint is set as expected self.assertEqual(write_endpoint, Test_globaldb_tests.write_location_host) self.assertEqual(read_endpoint, Test_globaldb_tests.write_location_host) writable_locations = [] readable_locations = [] write_endpoint, read_endpoint = client._global_endpoint_manager.UpdateLocationsCache(writable_locations, readable_locations) # If writable_locations and readable_locations are empty, both Read and Write Endpoints point to endpoint passed while creating the client instance self.assertEqual(write_endpoint, Test_globaldb_tests.host) self.assertEqual(read_endpoint, Test_globaldb_tests.host) writable_locations = [{'name' : Test_globaldb_tests.write_location, 'databaseAccountEndpoint' : Test_globaldb_tests.write_location_host}] readable_locations = [] write_endpoint, read_endpoint = client._global_endpoint_manager.UpdateLocationsCache(writable_locations, readable_locations) # If there are no readable_locations, we use the write endpoint as ReadEndpoint self.assertEqual(write_endpoint, Test_globaldb_tests.write_location_host) self.assertEqual(read_endpoint, Test_globaldb_tests.write_location_host) writable_locations = [] readable_locations = [{'name' : Test_globaldb_tests.read_location, 'databaseAccountEndpoint' : Test_globaldb_tests.read_location_host}] write_endpoint, read_endpoint = client._global_endpoint_manager.UpdateLocationsCache(writable_locations, readable_locations) # If there are no writable_locations, both Read and Write Endpoints point to endpoint passed while creating the client instance self.assertEqual(write_endpoint, Test_globaldb_tests.host) self.assertEqual(read_endpoint, Test_globaldb_tests.host) writable_locations = [{'name' : Test_globaldb_tests.write_location, 'databaseAccountEndpoint' : Test_globaldb_tests.write_location_host}] readable_locations = [{'name' : Test_globaldb_tests.read_location, 'databaseAccountEndpoint' : Test_globaldb_tests.read_location_host}, {'name' : Test_globaldb_tests.read_location2, 'databaseAccountEndpoint' : Test_globaldb_tests.read_location2_host}] connection_policy = documents.ConnectionPolicy() connection_policy.PreferredLocations = [Test_globaldb_tests.read_location2] client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) write_endpoint, read_endpoint = client._global_endpoint_manager.UpdateLocationsCache(writable_locations, readable_locations) # Test that the preferred location is set as ReadEndpoint instead of default write endpoint when no preference is set self.assertEqual(write_endpoint, Test_globaldb_tests.write_location_host) self.assertEqual(read_endpoint, Test_globaldb_tests.read_location2_host) writable_locations = [{'name' : Test_globaldb_tests.write_location, 'databaseAccountEndpoint' : Test_globaldb_tests.write_location_host}, {'name' : Test_globaldb_tests.read_location2, 'databaseAccountEndpoint' : Test_globaldb_tests.read_location2_host}] readable_locations = [{'name' : Test_globaldb_tests.read_location, 'databaseAccountEndpoint' : Test_globaldb_tests.read_location_host}] connection_policy = documents.ConnectionPolicy() connection_policy.PreferredLocations = [Test_globaldb_tests.read_location2] client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) write_endpoint, read_endpoint = client._global_endpoint_manager.UpdateLocationsCache(writable_locations, readable_locations) # Test that the preferred location is chosen from the WriteLocations if it's not present in the ReadLocations self.assertEqual(write_endpoint, Test_globaldb_tests.write_location_host) self.assertEqual(read_endpoint, Test_globaldb_tests.read_location2_host) writable_locations = [{'name' : Test_globaldb_tests.write_location, 'databaseAccountEndpoint' : Test_globaldb_tests.write_location_host}] readable_locations = [{'name' : Test_globaldb_tests.read_location, 'databaseAccountEndpoint' : Test_globaldb_tests.read_location_host}, {'name' : Test_globaldb_tests.read_location2, 'databaseAccountEndpoint' : Test_globaldb_tests.read_location2_host}] connection_policy.EnableEndpointDiscovery = False client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}, connection_policy) write_endpoint, read_endpoint = client._global_endpoint_manager.UpdateLocationsCache(writable_locations, readable_locations) # If EnableEndpointDiscovery is False, both Read and Write Endpoints point to endpoint passed while creating the client instance self.assertEqual(write_endpoint, Test_globaldb_tests.host) self.assertEqual(read_endpoint, Test_globaldb_tests.host) def test_globaldb_locational_endpoint_parser(self): url_endpoint='https://contoso.documents.azure.com:443/' location_name='East US' # Creating a locational endpoint from the location name using the parser method locational_endpoint = global_endpoint_manager._GlobalEndpointManager.GetLocationalEndpoint(url_endpoint, location_name) self.assertEqual(locational_endpoint, 'https://contoso-EastUS.documents.azure.com:443/') url_endpoint='https://Contoso.documents.azure.com:443/' location_name='East US' # Note that the host name gets lowercased as the urlparser in Python doesn't retains the casing locational_endpoint = global_endpoint_manager._GlobalEndpointManager.GetLocationalEndpoint(url_endpoint, location_name) self.assertEqual(locational_endpoint, 'https://contoso-EastUS.documents.azure.com:443/') def test_globaldb_endpoint_discovery_retry_policy_mock(self): client = cosmos_client.CosmosClient(Test_globaldb_tests.host, {'masterKey': Test_globaldb_tests.masterKey}) self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunction self.OriginalGetDatabaseAccount = client.GetDatabaseAccount client.GetDatabaseAccount = self._MockGetDatabaseAccount max_retry_attempt_count = 10 retry_after_in_milliseconds = 500 endpoint_discovery_retry_policy._EndpointDiscoveryRetryPolicy.Max_retry_attempt_count = max_retry_attempt_count endpoint_discovery_retry_policy._EndpointDiscoveryRetryPolicy.Retry_after_in_milliseconds = retry_after_in_milliseconds document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} self.__AssertHTTPFailureWithStatus( StatusCodes.FORBIDDEN, SubStatusCodes.WRITE_FORBIDDEN, client.CreateItem, self.test_coll['_self'], document_definition) retry_utility._ExecuteFunction = self.OriginalExecuteFunction def _MockExecuteFunction(self, function, *args, **kwargs): raise errors.HTTPFailure(StatusCodes.FORBIDDEN, "Write Forbidden", {'x-ms-substatus' : SubStatusCodes.WRITE_FORBIDDEN}) def _MockGetDatabaseAccount(self, url_conection): database_account = documents.DatabaseAccount() return database_account if __name__ == '__main__': unittest.main() azure-cosmos-python-3.1.1/test/location_cache_tests.py000066400000000000000000000563141352206500100231730ustar00rootroot00000000000000import unittest import uuid import threading import pytest from time import sleep from azure.cosmos.http_constants import ResourceType import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents from azure.cosmos.request_object import _RequestObject from azure.cosmos.location_cache import LocationCache from azure.cosmos.global_endpoint_manager import _GlobalEndpointManager import azure.cosmos.errors as errors from azure.cosmos.http_constants import StatusCodes, SubStatusCodes, HttpHeaders import azure.cosmos.retry_utility as retry_utility import six class RefreshThread(threading.Thread): def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, verbose=None): if six.PY2: super(RefreshThread, self).__init__(group=group, target=target, name=name, verbose=verbose) else: super().__init__() self.endpoint_manager = kwargs['endpoint_manager'] def run(self): self.endpoint_manager.force_refresh(None) @pytest.mark.usefixtures("teardown") class LocationCacheTest(unittest.TestCase): DEFAULT_ENDPOINT = "https://default.documents.azure.com" LOCATION_1_ENDPOINT = "https://location1.documents.azure.com" LOCATION_2_ENDPOINT = "https://location2.documents.azure.com" LOCATION_3_ENDPOINT = "https://location3.documents.azure.com" LOCATION_4_ENDPOINT = "https://location4.documents.azure.com" REFRESH_TIME_INTERVAL_IN_MS = 1000 endpoint_by_location = {"location1": LOCATION_1_ENDPOINT, "location2": LOCATION_2_ENDPOINT, "location3": LOCATION_3_ENDPOINT, "location4": LOCATION_4_ENDPOINT} def mock_create_db_with_flag_enabled(self, url_connection = None): self.database_account = self.create_database_account(True) return self.database_account def mock_create_db_with_flag_disabled(self, url_connection = None): self.database_account = self.create_database_account(False) return self.database_account def create_spy_client(self, use_multiple_write_locations, enable_endpoint_discovery, is_preferred_locations_list_empty): self.preferred_locations = ["location1", "location2", "location3", "location4"] connectionPolicy = documents.ConnectionPolicy() connectionPolicy.DisableSSLVerification = True connectionPolicy.PreferredLocations = [] if is_preferred_locations_list_empty else self.preferred_locations connectionPolicy.EnableEndpointDiscovery = enable_endpoint_discovery connectionPolicy.UseMultipleWriteLocations = use_multiple_write_locations client = cosmos_client.CosmosClient(self.DEFAULT_ENDPOINT, {'masterKey': "SomeKeyValue"}, connectionPolicy) return client def test_validate_retry_on_session_not_availabe_with_disable_multiple_write_locations_and_endpoint_discovery_disabled(self): self.validate_retry_on_session_not_availabe_with_endpoint_discovery_disabled(False, False, False) self.validate_retry_on_session_not_availabe_with_endpoint_discovery_disabled(False, False, True) self.validate_retry_on_session_not_availabe_with_endpoint_discovery_disabled(False, True, False) self.validate_retry_on_session_not_availabe_with_endpoint_discovery_disabled(False, True, True) self.validate_retry_on_session_not_availabe_with_endpoint_discovery_disabled(True, False, False) self.validate_retry_on_session_not_availabe_with_endpoint_discovery_disabled(True, False, True) self.validate_retry_on_session_not_availabe_with_endpoint_discovery_disabled(True, True, False) self.validate_retry_on_session_not_availabe_with_endpoint_discovery_disabled(True, True, True) def validate_retry_on_session_not_availabe_with_endpoint_discovery_disabled(self, is_preferred_locations_list_empty, use_multiple_write_locations, is_read_request): self.counter = 0 self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunctionSessionReadFailureOnce self.original_get_database_account = cosmos_client.CosmosClient.GetDatabaseAccount cosmos_client.CosmosClient.GetDatabaseAccount = self.mock_create_db_with_flag_enabled if use_multiple_write_locations else self.mock_create_db_with_flag_disabled enable_endpoint_discovery = False client = self.create_spy_client(use_multiple_write_locations, enable_endpoint_discovery, is_preferred_locations_list_empty) try: if is_read_request: client.ReadItem("dbs/mydb/colls/mycoll/docs/1") else: client.CreateItem("dbs/mydb/colls/mycoll/", {'id':'1'}) self.fail() except errors.HTTPFailure as e: # not retried self.assertEqual(self.counter, 1) self.counter = 0 self.assertEqual(e.status_code, StatusCodes.NOT_FOUND) self.assertEqual(e.sub_status, SubStatusCodes.READ_SESSION_NOTAVAILABLE) cosmos_client.CosmosClient.GetDatabaseAccount = self.original_get_database_account retry_utility._ExecuteFunction = self.OriginalExecuteFunction def _MockExecuteFunctionSessionReadFailureOnce(self, function, *args, **kwargs): self.counter += 1 raise errors.HTTPFailure(StatusCodes.NOT_FOUND, "Read Session not available", {HttpHeaders.SubStatus: SubStatusCodes.READ_SESSION_NOTAVAILABLE}) def test_validate_retry_on_session_not_availabe_with_endpoint_discovery_enabled(self): # sequence of chosen endpoints: # 1. Single region, No Preferred Location: # location1 (default) -> location1 (no preferred location, hence default) # 2. Single Region, Preferred Locations present: # location1 (1st preferred location) -> location1 (1st location in DBA's WriteLocation) # 3. MultiRegion, Preferred Regions present: # location1 (1st preferred location Read Location) -> location1 (1st location in DBA's WriteLocation) -> # location2 (2nd preferred location Read Location)-> location4 (3rd preferred location Read Location) #self.validate_retry_on_session_not_availabe(True, False) #self.validate_retry_on_session_not_availabe(False, False) self.validate_retry_on_session_not_availabe(False, True) def validate_retry_on_session_not_availabe(self, is_preferred_locations_list_empty, use_multiple_write_locations): self.counter = 0 self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunctionSessionReadFailureTwice self.original_get_database_account = cosmos_client.CosmosClient.GetDatabaseAccount cosmos_client.CosmosClient.GetDatabaseAccount = self.mock_create_db_with_flag_enabled if use_multiple_write_locations else self.mock_create_db_with_flag_disabled enable_endpoint_discovery = True self.is_preferred_locations_list_empty = is_preferred_locations_list_empty self.use_multiple_write_locations = use_multiple_write_locations client = self.create_spy_client(use_multiple_write_locations, enable_endpoint_discovery, is_preferred_locations_list_empty) try: client.ReadItem("dbs/mydb/colls/mycoll/docs/1") except errors.HTTPFailure as e: # not retried self.assertEqual(self.counter, 4 if use_multiple_write_locations else 2) self.counter = 0 self.assertEqual(e.status_code, StatusCodes.NOT_FOUND) self.assertEqual(e.sub_status, SubStatusCodes.READ_SESSION_NOTAVAILABLE) cosmos_client.CosmosClient.GetDatabaseAccount = self.original_get_database_account retry_utility._ExecuteFunction = self.OriginalExecuteFunction def _MockExecuteFunctionSessionReadFailureTwice(self, function, *args, **kwargs): request = args[1] if self.counter == 0: if not self.use_multiple_write_locations: expected_endpoint = self.database_account.WritableLocations[0]['databaseAccountEndpoint'] if self.is_preferred_locations_list_empty else self.preferred_locations[0] else: expected_endpoint = self.endpoint_by_location[self.preferred_locations[0]] self.assertFalse(request.should_clear_session_token_on_session_read_failure) elif self.counter == 1: expected_endpoint = self.database_account.WritableLocations[0]['databaseAccountEndpoint'] if not self.use_multiple_write_locations: self.assertTrue(request.should_clear_session_token_on_session_read_failure) else: self.assertFalse(request.should_clear_session_token_on_session_read_failure) elif self.counter == 2: expected_endpoint = self.endpoint_by_location[self.preferred_locations[1]] self.assertFalse(request.should_clear_session_token_on_session_read_failure) elif self.counter == 3: expected_endpoint = self.database_account.ReadableLocations[2]['databaseAccountEndpoint'] self.assertTrue(request.should_clear_session_token_on_session_read_failure) self.assertEqual(expected_endpoint, request.location_endpoint_to_route) self.counter += 1 raise errors.HTTPFailure(StatusCodes.NOT_FOUND, "Read Session not available", {HttpHeaders.SubStatus: SubStatusCodes.READ_SESSION_NOTAVAILABLE}) def test_validate_location_cache(self): self.original_get_database_account = cosmos_client.CosmosClient.GetDatabaseAccount cosmos_client.CosmosClient.GetDatabaseAccount = self.mock_get_database_account self.get_database_account_hit_counter = 0 for i in range (0,8): use_multiple_write_locations = (i & 1) > 0 endpoint_discovery_enabled = (i & 2) > 0 is_preferred_list_empty = (i & 4) > 0 self.validate_location_cache(use_multiple_write_locations, endpoint_discovery_enabled, is_preferred_list_empty) cosmos_client.CosmosClient.GetDatabaseAccount = self.original_get_database_account def test_validate_write_endpoint_order_with_client_side_disable_multiple_write_location(self): self.original_get_database_account = cosmos_client.CosmosClient.GetDatabaseAccount cosmos_client.CosmosClient.GetDatabaseAccount = self.mock_get_database_account self.get_database_account_hit_counter = 0 self.initialize(False, True, False) self.assertEqual(self.location_cache.get_write_endpoints()[0], self.LOCATION_1_ENDPOINT) self.assertEqual(self.location_cache.get_write_endpoints()[1], self.LOCATION_2_ENDPOINT) self.assertEqual(self.location_cache.get_write_endpoints()[2], self.LOCATION_3_ENDPOINT) cosmos_client.CosmosClient.GetDatabaseAccount = self.original_get_database_account def mock_get_database_account(self, url_connection = None): self.get_database_account_hit_counter += 1 return self.create_database_account(True) def create_database_account(self, use_multiple_write_locations): database_account = documents.DatabaseAccount() database_account._EnableMultipleWritableLocations = use_multiple_write_locations database_account._WritableLocations = [ {'name': 'location1', 'databaseAccountEndpoint': self.LOCATION_1_ENDPOINT}, {'name': 'location2', 'databaseAccountEndpoint': self.LOCATION_2_ENDPOINT}, {'name': 'location3', 'databaseAccountEndpoint': self.LOCATION_3_ENDPOINT} ] database_account._ReadableLocations = [ {'name': 'location1', 'databaseAccountEndpoint': self.LOCATION_1_ENDPOINT}, {'name': 'location2', 'databaseAccountEndpoint': self.LOCATION_2_ENDPOINT}, {'name': 'location4', 'databaseAccountEndpoint': self.LOCATION_4_ENDPOINT} ] return database_account def initialize(self, use_multiple_write_locations, enable_endpoint_discovery, is_preferred_locations_list_empty): self.database_account = self.create_database_account(use_multiple_write_locations) preferred_locations = ["location1", "location2", "location3"] self.preferred_locations = [] if is_preferred_locations_list_empty else preferred_locations self.location_cache = LocationCache( self.preferred_locations, self.DEFAULT_ENDPOINT, enable_endpoint_discovery, use_multiple_write_locations, self.REFRESH_TIME_INTERVAL_IN_MS) self.location_cache.perform_on_database_account_read(self.database_account) connectionPolicy = documents.ConnectionPolicy() connectionPolicy.PreferredLocations = self.preferred_locations client = cosmos_client.CosmosClient("", {}, connectionPolicy) self.global_endpoint_manager = client._global_endpoint_manager def validate_location_cache(self, use_multiple_write_locations, endpoint_discovery_enabled, is_preferred_list_empty): for write_location_index in range(0,3): for read_location_index in range(0,2): self.initialize(use_multiple_write_locations, endpoint_discovery_enabled, is_preferred_list_empty) current_write_endpoints = self.location_cache.get_write_endpoints() current_read_endpoints = self.location_cache.get_read_endpoints() for i in range(0, read_location_index): self.location_cache.mark_endpoint_unavailable_for_read(self.database_account.ReadableLocations[i]['databaseAccountEndpoint']) self.global_endpoint_manager.mark_endpoint_unavailable_for_read(self.database_account.ReadableLocations[i]['databaseAccountEndpoint']) for i in range(0, write_location_index): self.location_cache.mark_endpoint_unavailable_for_write(self.database_account.WritableLocations[i]['databaseAccountEndpoint']) self.global_endpoint_manager.mark_endpoint_unavailable_for_write(self.database_account.WritableLocations[i]['databaseAccountEndpoint']) write_endpoint_by_location = {} for dba_location in self.database_account._WritableLocations: write_endpoint_by_location[dba_location['name']] = dba_location['databaseAccountEndpoint'] read_endpoint_by_location = {} for dba_location in self.database_account._ReadableLocations: read_endpoint_by_location[dba_location['name']] = dba_location['databaseAccountEndpoint'] available_write_endpoints = [] for i in range(write_location_index, len(self.preferred_locations)): location = self.preferred_locations[i] endpoint = write_endpoint_by_location[location] if location in write_endpoint_by_location else None if endpoint: available_write_endpoints.append(endpoint) available_read_endpoints = [] for i in range(read_location_index, len(self.preferred_locations)): location = self.preferred_locations[i] endpoint = read_endpoint_by_location[location] if location in read_endpoint_by_location else None if endpoint: available_read_endpoints.append(endpoint) self.validate_endpoint_refresh(use_multiple_write_locations, endpoint_discovery_enabled, available_write_endpoints, available_read_endpoints, write_location_index > 0) self.validate_global_endpoint_location_cache_refresh() self.validate_request_endpoint_resolution(use_multiple_write_locations, endpoint_discovery_enabled, available_write_endpoints, available_read_endpoints) # wait for TTL on unavailablity info sleep(1.5) self.assertEquals(current_write_endpoints, self.location_cache.get_write_endpoints()) self.assertEquals(current_read_endpoints, self.location_cache.get_read_endpoints()) def validate_global_endpoint_location_cache_refresh(self): self.get_database_account_hit_counter = 0 refresh_threads = [] for i in range(10): refresh_thread = RefreshThread(kwargs={'endpoint_manager':self.global_endpoint_manager}) refresh_thread.start() refresh_threads.append(refresh_thread) for i in range(10): refresh_threads[i].join() self.assertTrue(self.get_database_account_hit_counter <= 1) for i in range(10): refresh_thread = RefreshThread(kwargs={'endpoint_manager':self.global_endpoint_manager}) refresh_thread.start() refresh_thread.join() self.assertTrue(self.get_database_account_hit_counter <= 1) def validate_endpoint_refresh(self, use_multiple_write_locations, endpoint_discovery_enabled, preferred_available_write_endpoints, preferred_available_read_endpoints, is_first_write_endpoint_unavailable): should_refresh_endpoints = self.location_cache.should_refresh_endpoints() is_most_preferred_location_unavailable_for_read = False is_most_preferred_location_unavailable_for_write = False if use_multiple_write_locations else is_first_write_endpoint_unavailable if (len(self.preferred_locations) > 0): most_preferred_read_location_name = None for preferred_location in self.preferred_locations: for read_location in self.database_account._ReadableLocations: if read_location['name'] == preferred_location: most_preferred_read_location_name = preferred_location break if most_preferred_read_location_name: break most_preferred_read_endpoint = self.endpoint_by_location[most_preferred_read_location_name] is_most_preferred_location_unavailable_for_read = True if len(preferred_available_read_endpoints) == 0 else preferred_available_read_endpoints[0] != most_preferred_read_endpoint most_preferred_write_location_name = None for preferred_location in self.preferred_locations: for write_location in self.database_account._WritableLocations: if write_location['name'] == preferred_location: most_preferred_write_location_name = preferred_location break if most_preferred_write_location_name: break most_preferred_write_endpoint = self.endpoint_by_location[most_preferred_write_location_name] if use_multiple_write_locations: is_most_preferred_location_unavailable_for_write = True if len(preferred_available_write_endpoints) == 0 else preferred_available_write_endpoints[0] != most_preferred_write_endpoint if not endpoint_discovery_enabled: self.assertFalse(should_refresh_endpoints) else: self.assertEquals(is_most_preferred_location_unavailable_for_read or is_most_preferred_location_unavailable_for_write, should_refresh_endpoints) def validate_request_endpoint_resolution(self, use_multiple_write_locations, endpoint_discovery_enabled, available_write_endpoints, available_read_endpoints): write_locations = self.database_account._WritableLocations if not endpoint_discovery_enabled: first_available_write_endpoint = self.DEFAULT_ENDPOINT second_available_write_endpoint = self.DEFAULT_ENDPOINT elif not use_multiple_write_locations: first_available_write_endpoint = write_locations[0]['databaseAccountEndpoint'] second_available_write_endpoint = write_locations[1]['databaseAccountEndpoint'] elif len(available_write_endpoints) > 1: first_available_write_endpoint = available_write_endpoints[0] second_available_write_endpoint = available_write_endpoints[1] elif len(available_write_endpoints) > 0: first_available_write_endpoint = available_write_endpoints[0] write_endpoint = write_locations[0]['databaseAccountEndpoint'] second_available_write_endpoint = write_endpoint if write_endpoint != first_available_write_endpoint else available_write_endpoints[1] else: first_available_write_endpoint = self.DEFAULT_ENDPOINT second_available_write_endpoint = self.DEFAULT_ENDPOINT if not endpoint_discovery_enabled: first_available_read_endpoint = self.DEFAULT_ENDPOINT elif len(self.preferred_locations) == 0: first_available_read_endpoint = first_available_write_endpoint elif len(available_read_endpoints) > 0: first_available_read_endpoint = available_read_endpoints[0] else: first_available_read_endpoint = self.endpoint_by_location[self.preferred_locations[0]] first_write_enpoint = self.DEFAULT_ENDPOINT if not endpoint_discovery_enabled else self.database_account.WritableLocations[0]['databaseAccountEndpoint'] second_write_enpoint = self.DEFAULT_ENDPOINT if not endpoint_discovery_enabled else self.database_account.WritableLocations[1]['databaseAccountEndpoint'] # If current write endpoint is unavailable, write endpoints order doesn't change # All write requests flip-flop between current write and alternate write endpoint write_endpoints = self.location_cache.get_write_endpoints() self.assertTrue(first_available_write_endpoint == write_endpoints[0]) self.assertTrue(second_available_write_endpoint == self.resolve_endpoint_for_write_request(ResourceType.Document, True)) self.assertTrue(first_available_write_endpoint == self.resolve_endpoint_for_write_request(ResourceType.Document, False)) # Writes to other resource types should be directed to first/second write endpoint self.assertTrue(first_write_enpoint == self.resolve_endpoint_for_write_request(ResourceType.Database, False)) self.assertTrue(second_write_enpoint == self.resolve_endpoint_for_write_request(ResourceType.Database, True)) # Reads should be directed to available read endpoints regardless of resource type self.assertTrue(first_available_read_endpoint == self.resolve_endpoint_for_read_request(True)) self.assertTrue(first_available_read_endpoint == self.resolve_endpoint_for_read_request(False)) def resolve_endpoint_for_read_request(self, master_resource_type): operation_type = documents._OperationType.Read resource_type = ResourceType.Database if master_resource_type else ResourceType.Document request = _RequestObject(resource_type, operation_type) return self.location_cache.resolve_service_endpoint(request) def resolve_endpoint_for_write_request(self, resource_type, use_alternate_write_endpoint): operation_type = documents._OperationType.Create request = _RequestObject(resource_type, operation_type) request.route_to_location_with_preferred_location_flag(1 if use_alternate_write_endpoint else 0, ResourceType.IsCollectionChild(resource_type)) return self.location_cache.resolve_service_endpoint(request) azure-cosmos-python-3.1.1/test/multiOrderbyTests.py000066400000000000000000000306071352206500100225170ustar00rootroot00000000000000# The MIT License (MIT) # Copyright (c) 2014 Microsoft Corporation # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import unittest import uuid import pytest import random import azure.cosmos.cosmos_client as cosmos_client import test.test_config as test_config # IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos account. # Collections are billing entities. By running these test cases, you may incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class MultiOrderbyTests(unittest.TestCase): """Multi Orderby and Composite Indexes Tests. """ NUMBER_FIELD = "numberField" STRING_FIELD = "stringField" NUMBER_FIELD_2 = "numberField2" STRING_FIELD_2 = "stringField2" BOOL_FIELD = "boolField" NULL_FIELD = "nullField" OBJECT_FIELD = "objectField" ARRAY_FIELD = "arrayField" SHORT_STRING_FIELD = "shortStringField" MEDIUM_STRING_FIELD = "mediumStringField" LONG_STRING_FIELD = "longStringField" PARTITION_KEY = "pk" documents = [] host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy client = cosmos_client.CosmosClient(host, {'masterKey': masterKey}, connectionPolicy) database = test_config._test_config.create_database_if_not_exist(client) def generate_multi_orderby_document(self): document = {} document['id'] = str(uuid.uuid4()) document[self.NUMBER_FIELD] = random.randint(0, 5) document[self.NUMBER_FIELD_2] = random.randint(0, 5) document[self.BOOL_FIELD] = random.randint(0, 2) % 2 == 0 document[self.STRING_FIELD] = str(random.randint(0, 5)) document[self.STRING_FIELD_2] = str(random.randint(0, 5)) document[self.NULL_FIELD] = None document[self.OBJECT_FIELD] = "" document[self.ARRAY_FIELD] = [] document[self.SHORT_STRING_FIELD] = "a" + str(random.randint(0, 100)) document[self.MEDIUM_STRING_FIELD] = "a" + str(random.randint(0, 128) + 100) document[self.LONG_STRING_FIELD] = "a" + str(random.randint(0, 255) + 128) document[self.PARTITION_KEY] = random.randint(0, 5) return document def create_random_documents(self, container, number_of_documents, number_of_duplicates): for i in range(0, number_of_documents): multi_orderby_document = self.generate_multi_orderby_document() for j in range(0, number_of_duplicates): # Add the document itself for exact duplicates clone = multi_orderby_document.copy() clone['id'] = str(uuid.uuid4()) self.documents.append(clone) # Permute all the fields so that there are duplicates with tie breaks number_clone = multi_orderby_document.copy() number_clone[self.NUMBER_FIELD] = random.randint(0, 5) number_clone['id'] = str(uuid.uuid4()) self.documents.append(number_clone) string_clone = multi_orderby_document.copy() string_clone[self.STRING_FIELD] = str(random.randint(0, 5)) string_clone['id'] = str(uuid.uuid4()) self.documents.append(string_clone) bool_clone = multi_orderby_document.copy() bool_clone[self.BOOL_FIELD] = random.randint(0, 2) % 2 == 0 bool_clone['id'] = str(uuid.uuid4()) self.documents.append(bool_clone) # Also fuzz what partition it goes to partition_clone = multi_orderby_document.copy() partition_clone[self.PARTITION_KEY] = random.randint(0, 5) partition_clone['id'] = str(uuid.uuid4()) self.documents.append(partition_clone) for document in self.documents: self.client.CreateItem(container['_self'], document) def test_multi_orderby_queries(self): indexingPolicy = { "indexingMode": "consistent", "automatic": True, "includedPaths": [ { "path": "/*", "indexes": [] } ], "excludedPaths": [ { "path": "/\"_etag\"/?" } ], "compositeIndexes": [ [ { "path": "/numberField", "order": "ascending" }, { "path": "/stringField", "order": "descending" } ], [ { "path": "/numberField", "order": "descending" }, { "path": "/stringField", "order": "ascending" }, { "path": "/numberField2", "order": "descending" }, { "path": "/stringField2", "order": "ascending" } ], [ { "path": "/numberField", "order": "descending" }, { "path": "/stringField", "order": "ascending" }, { "path": "/boolField", "order": "descending" }, { "path": "/nullField", "order": "ascending" } ], [ { "path": "/stringField", "order": "ascending" }, { "path": "/shortStringField", "order": "ascending" }, { "path": "/mediumStringField", "order": "ascending" }, { "path": "/longStringField", "order": "ascending" } ] ] } partitionKey = { "paths": [ "/pk" ], "kind": "Hash" } container_id = 'multi_orderby_container' + str(uuid.uuid4()) container_definition = { 'id': container_id, 'indexingPolicy': indexingPolicy, 'partitionKey': partitionKey } options = { 'offerThroughput': 25100 } created_container = self.client.CreateContainer(self.database['_self'], container_definition, options) number_of_documents = 4 number_of_duplicates = 5 self.create_random_documents(created_container, number_of_documents, number_of_duplicates) feed_options = { 'enableCrossPartitionQuery': True } bool_vals = [True, False] composite_indexes = indexingPolicy['compositeIndexes'] for composite_index in composite_indexes: # for every order for invert in bool_vals: # for normal and inverted order for has_top in bool_vals: # with and without top for has_filter in bool_vals: # with and without filter # Generate a multi order by from that index orderby_items = [] select_items = [] for composite_path in composite_index: is_desc = True if composite_path['order'] == "descending" else False if invert: is_desc = not is_desc is_desc_string = "DESC" if is_desc else "ASC" composite_path_name = composite_path['path'].replace("/", "") orderby_items_string = "root." + composite_path_name + " " + is_desc_string select_items_string = "root." + composite_path_name orderby_items.append(orderby_items_string) select_items.append(select_items_string) top_count = 10 select_item_builder = "" for select_item in select_items: select_item_builder += select_item + "," select_item_builder = select_item_builder[:-1] orderby_item_builder = "" for orderby_item in orderby_items: orderby_item_builder += orderby_item + "," orderby_item_builder = orderby_item_builder[:-1] top_string = "TOP " + str(top_count) if has_top else "" where_string = "WHERE root." + self.NUMBER_FIELD + " % 2 = 0" if has_filter else "" query = "SELECT " + top_string + " [" + select_item_builder + "] " + \ "FROM root " + where_string + " " + \ "ORDER BY " + orderby_item_builder expected_ordered_list = self.top(self.sort(self.filter(self.documents, has_filter), composite_index, invert), has_top, top_count) result_ordered_list = list(self.client.QueryItems(created_container['_self'], query, feed_options)) self.validate_results(expected_ordered_list, result_ordered_list, composite_index) def top(self, documents, has_top, top_count): return documents[0:top_count] if has_top else documents def sort(self, documents, composite_index, invert): current_docs = documents for composite_path in reversed(composite_index): order = composite_path['order'] if invert: order = "ascending" if order == "descending" else "descending" path = composite_path['path'].replace("/", "") if self.NULL_FIELD not in path: current_docs = sorted(current_docs, key=lambda x: x[path], reverse=True if order == "descending" else False) return current_docs def filter(self, documents, has_filter): return [x for x in documents if x[self.NUMBER_FIELD] % 2 == 0] if has_filter else documents def validate_results(self, expected_ordered_list, result_ordered_list, composite_index): self.assertEquals(len(expected_ordered_list), len(result_ordered_list)) for i in range(0, len(expected_ordered_list)): result_values = result_ordered_list[i]['$1'] self.assertEquals(len(result_values), len(composite_index)) for j in range(0, len(composite_index)): path = composite_index[j]['path'].replace("/", "") if self.NULL_FIELD in path: self.assertIsNone(expected_ordered_list[i][path]) self.assertIsNone(result_values[j]) else: self.assertEquals(expected_ordered_list[i][path], result_values[j]) azure-cosmos-python-3.1.1/test/multimaster_tests.py000066400000000000000000000105441352206500100226010ustar00rootroot00000000000000import json import os.path import unittest import uuid import pytest import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents import azure.cosmos.errors as errors import azure.cosmos.base as base import azure.cosmos.constants as constants import azure.cosmos.retry_options as retry_options from azure.cosmos.http_constants import HttpHeaders, StatusCodes, SubStatusCodes import azure.cosmos.retry_utility as retry_utility import test.test_config as test_config @pytest.mark.usefixtures("teardown") class MultiMasterTests(unittest.TestCase): host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy counter = 0 last_headers = [] def test_tentative_writes_header_present(self): self.last_headers = [] self.EnableMultipleWritableLocations = True self._validate_tentative_write_headers() def test_tentative_writes_header_not_present(self): self.last_headers = [] self.EnableMultipleWritableLocations = False self._validate_tentative_write_headers() def _validate_tentative_write_headers(self): self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunction connectionPolicy = MultiMasterTests.connectionPolicy connectionPolicy.UseMultipleWriteLocations = True client = cosmos_client.CosmosClient(MultiMasterTests.host, {'masterKey': MultiMasterTests.masterKey}, connectionPolicy) created_collection = test_config._test_config.create_multi_partition_collection_with_custom_pk_if_not_exist(client) document_definition = { 'id': 'doc' + str(uuid.uuid4()), 'pk': 'pk', 'name': 'sample document', 'operation': 'insertion'} created_document = client.CreateItem(created_collection['_self'], document_definition) sproc_definition = { 'id': 'sample sproc' + str(uuid.uuid4()), 'serverScript': 'function() {var x = 10;}' } sproc = client.CreateStoredProcedure(created_collection['_self'], sproc_definition) client.ExecuteStoredProcedure(sproc['_self'], None, {'partitionKey':'pk'}) client.ReadItem(created_document['_self'], {'partitionKey':'pk'}) created_document['operation'] = 'replace' replaced_document = client.ReplaceItem(created_document['_self'], created_document) replaced_document['operation'] = 'upsert' upserted_document = client.UpsertItem(created_collection['_self'], replaced_document) client.DeleteItem(upserted_document['_self'], {'partitionKey':'pk'}) is_allow_tentative_writes_set = self.EnableMultipleWritableLocations == True # Create Document - Makes one initial call to fetch collection self.assertEqual(self.last_headers[0], is_allow_tentative_writes_set) self.assertEqual(self.last_headers[1], is_allow_tentative_writes_set) # Create Stored procedure self.assertEqual(self.last_headers[2], is_allow_tentative_writes_set) # Execute Stored procedure self.assertEqual(self.last_headers[3], is_allow_tentative_writes_set) # Read Document self.assertEqual(self.last_headers[4], is_allow_tentative_writes_set) # Replace Document self.assertEqual(self.last_headers[5], is_allow_tentative_writes_set) # Upsert Document self.assertEqual(self.last_headers[6], is_allow_tentative_writes_set) # Delete Document self.assertEqual(self.last_headers[7], is_allow_tentative_writes_set) retry_utility._ExecuteFunction = self.OriginalExecuteFunction def _MockExecuteFunction(self, function, *args, **kwargs): self.counter += 1 if self.counter == 1: return {constants._Constants.EnableMultipleWritableLocations: self.EnableMultipleWritableLocations}, {} else: if len(args) > 0: self.last_headers.append(HttpHeaders.AllowTentativeWrites in args[5]['headers'] and args[5]['headers'][HttpHeaders.AllowTentativeWrites] == 'true') return self.OriginalExecuteFunction(function, *args, **kwargs) if __name__ == '__main__': unittest.main() azure-cosmos-python-3.1.1/test/orderby_tests.py000066400000000000000000000572221352206500100217050ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import uuid import pytest import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client from azure.cosmos import query_iterable import azure.cosmos.base as base from six.moves import xrange import test.test_config as test_config #IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos account. # Collections are billing entities. By running these test cases, you may incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class CrossPartitionTopOrderByTest(unittest.TestCase): """Orderby Tests. """ host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy @classmethod def setUpClass(cls): # creates the database, collection, and insert all the documents # we will gain some speed up in running the tests by creating the database, collection and inserting all the docs only once if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_ENDPOINT_HERE]'): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") cls.client = cosmos_client.CosmosClient(cls.host, {'masterKey': cls.masterKey}, cls.connectionPolicy) cls.created_db = test_config._test_config.create_database_if_not_exist(cls.client) cls.created_collection = CrossPartitionTopOrderByTest.create_collection(cls.client, cls.created_db) cls.collection_link = cls.GetDocumentCollectionLink(cls.created_db, cls.created_collection) # create a document using the document definition cls.document_definitions = [] for i in xrange(20): d = {'id' : str(i), 'name': 'sample document', 'spam': 'eggs' + str(i), 'cnt': i, 'key': 'value', 'spam2': 'eggs' + str(i) if (i == 3) else i, 'boolVar': (i % 2 == 0), 'number': 1.1 * i } cls.document_definitions.append(d) CrossPartitionTopOrderByTest.insert_doc() @classmethod def tearDownClass(cls): cls.client.DeleteContainer(cls.collection_link) def setUp(self): # sanity check: partition_key_ranges = list(self.client._ReadPartitionKeyRanges(self.collection_link)) self.assertGreaterEqual(len(partition_key_ranges), 5) # sanity check: read documents after creation queried_docs = list(self.client.ReadItems(self.collection_link)) self.assertEqual( len(queried_docs), len(self.document_definitions), 'create should increase the number of documents') def test_orderby_query(self): # test a simply order by query # an order by query query = { 'query': 'SELECT * FROM root r order by r.spam', } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['spam'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key)] # validates the results size and order self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_query_as_string(self): # test a simply order by query as string # an order by query query = 'SELECT * FROM root r order by r.spam' options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['spam'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key)] # validates the results size and order self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_asc_query(self): # test an order by query with explicit ascending ordering # an ascending order by query (ascending explicitly mentioned in the query) query = { 'query': 'SELECT * FROM root r order by r.spam ASC', } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['spam'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key)] # validates the results size and order self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_desc_query(self): # test an order by query with explicit descending ordering # a descending order by query query = { 'query': 'SELECT * FROM root r order by r.spam DESC', } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['spam'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key, reverse=True)] # validates the results size and order self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_top_query(self): # test an order by query combined with top top_count = 9 # sanity check self.assertLess(top_count, len(self.document_definitions)) # an order by query with top, total existing docs more than requested top count query = { 'query': 'SELECT top %d * FROM root r order by r.spam' % top_count } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['spam'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key)[:top_count]] self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_top_query_less_results_than_top_counts(self): # test an order by query combined with top. where top is greater than the total number of docs top_count = 30 # sanity check self.assertGreater(top_count, len(self.document_definitions)) # an order by query with top, total existing docs less than requested top count query = { 'query': 'SELECT top %d * FROM root r order by r.spam' % top_count } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['spam'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key)] self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_top_query(self): # test a simple top query without order by. # The rewrittenQuery in the query execution info responded by backend will be empty partition_key_ranges = list(self.client._ReadPartitionKeyRanges(self.collection_link)) docs_by_partition_key_range_id = self.find_docs_by_partition_key_range_id() # find the first two non-empty target partition key ranges cnt = 0 first_two_ranges_results = [] for r in partition_key_ranges: if cnt >= 2: break p_id = r['id'] if len(docs_by_partition_key_range_id[p_id]) > 0: first_two_ranges_results.extend(docs_by_partition_key_range_id[p_id]) cnt += 1 # sanity checks self.assertEqual(cnt, 2) self.assertLess(2, len(partition_key_ranges)) options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 # sanity check self.assertLess(len(first_two_ranges_results), len(self.document_definitions)) self.assertGreater(len(first_two_ranges_results), 1) expected_ordered_ids = [d['id'] for d in first_two_ranges_results] # a top query, the results will be sorted based on the target partition key range query = { 'query': 'SELECT top %d * FROM root r' % len(expected_ordered_ids) } self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_top_query_as_string(self): # test a simple top query without order by. # The rewrittenQuery in the query execution info responded by backend will be empty partition_key_ranges = list(self.client._ReadPartitionKeyRanges(self.collection_link)) docs_by_partition_key_range_id = self.find_docs_by_partition_key_range_id() # find the first two non-empty target partition key ranges cnt = 0 first_two_ranges_results = [] for r in partition_key_ranges: if cnt >= 2: break p_id = r['id'] if len(docs_by_partition_key_range_id[p_id]) > 0: first_two_ranges_results.extend(docs_by_partition_key_range_id[p_id]) cnt += 1 # sanity checks self.assertEqual(cnt, 2) self.assertLess(2, len(partition_key_ranges)) options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 # sanity check self.assertLess(len(first_two_ranges_results), len(self.document_definitions)) self.assertGreater(len(first_two_ranges_results), 1) expected_ordered_ids = [d['id'] for d in first_two_ranges_results] # a top query, the results will be sorted based on the target partition key range query = 'SELECT top %d * FROM root r' % len(expected_ordered_ids) self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_parametrized_top_query(self): # test a simple parameterized query without order by. # The rewrittenQuery in the query execution info responded by backend will be empty partition_key_ranges = list(self.client._ReadPartitionKeyRanges(self.collection_link)) docs_by_partition_key_range_id = self.find_docs_by_partition_key_range_id() # find the first two non-empty target partition key ranges cnt = 0 first_two_ranges_results = [] for r in partition_key_ranges: if cnt >= 2: break p_id = r['id'] if len(docs_by_partition_key_range_id[p_id]) > 0: first_two_ranges_results.extend(docs_by_partition_key_range_id[p_id]) cnt += 1 # sanity checks self.assertEqual(cnt, 2) self.assertLess(2, len(partition_key_ranges)) options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 # sanity check self.assertLess(len(first_two_ranges_results), len(self.document_definitions)) self.assertGreater(len(first_two_ranges_results), 1) expected_ordered_ids = [d['id'] for d in first_two_ranges_results] # a top query, the results will be sorted based on the target partition key range query = { 'query': 'SELECT top @n * FROM root r', "parameters": [ {"name": "@n", "value": len(expected_ordered_ids)} ] } self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_query_with_parametrized_top(self): # test an order by query combined with parametrized top top_count = 9 # sanity check self.assertLess(top_count, len(self.document_definitions)) options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['spam'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key)[:top_count]] # a parametrized top order by query query = { 'query': 'SELECT top @n * FROM root r order by r.spam', "parameters": [ {"name": "@n", "value": top_count} ] } self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_query_with_parametrized_predicate(self): # test an order by query combined with parametrized predicate options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 # an order by query with parametrized predicate query = { 'query': 'SELECT * FROM root r where r.cnt > @cnt order by r.spam', "parameters": [ {"name": "@cnt", "value": 5} ] } def get_order_by_key(r): return r['spam'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key) if r['cnt'] > 5] self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_query_noncomparable_orderby_item(self): # test orderby with different order by item type # an order by query query = { 'query': 'SELECT * FROM root r order by r.spam2 DESC', } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['id'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key)] # validates the results size and order try: self.execute_query_and_validate_results(query, options, expected_ordered_ids) self.fail('non comparable order by items did not result in failure.') except ValueError as e: self.assertTrue(e.args[0] == "Expected String, but got Number." or e.message == "Expected Number, but got String.") def test_orderby_integer_query(self): # an order by integer query query = { 'query': 'SELECT * FROM root r order by r.cnt', } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['cnt'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key)] # validates the results size and order self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_floating_point_number_query(self): # an orderby by floating point number query query = { 'query': 'SELECT * FROM root r order by r.number', } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 def get_order_by_key(r): return r['number'] expected_ordered_ids = [r['id'] for r in sorted(self.document_definitions, key=get_order_by_key)] # validates the results size and order self.execute_query_and_validate_results(query, options, expected_ordered_ids) def test_orderby_boolean_query(self): # an orderby by floating point number query query = { 'query': 'SELECT * FROM root r order by r.boolVar', } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 result_iterable = self.client.QueryItems(self.collection_link, query, options) results = list(result_iterable) # validates the results size and order self.assertEqual(len(results), len(self.document_definitions)) # false values before true values index = 0 while index < len(results): if results[index]['boolVar']: break self.assertTrue(int(results[index]['id']) % 2 == 1) index = index + 1 while index < len(results): self.assertTrue(results[index]['boolVar']) self.assertTrue(int(results[index]['id']) % 2 == 0) index = index + 1 def find_docs_by_partition_key_range_id(self): query = { 'query': 'SELECT * FROM root r' } partition_key_range = list(self.client._ReadPartitionKeyRanges(self.collection_link)) docs_by_partition_key_range_id = {} for r in partition_key_range: options = {} path = base.GetPathFromLink(self.collection_link, 'docs') collection_id = base.GetResourceIdOrFullNameFromLink(self.collection_link) def fetch_fn(options): return self.client.QueryFeed(path, collection_id, query, options, r['id']) docResultsIterable = query_iterable.QueryIterable(self.client, query, options, fetch_fn, self.collection_link) docs = list(docResultsIterable) self.assertFalse(r['id'] in docs_by_partition_key_range_id) docs_by_partition_key_range_id[r['id']] = docs return docs_by_partition_key_range_id def execute_query_and_validate_results(self, query, options, expected_ordered_ids): # executes the query and validates the results against the expected results page_size = options['maxItemCount'] result_iterable = self.client.QueryItems(self.collection_link, query, options) self.assertTrue(isinstance(result_iterable, query_iterable.QueryIterable)) ###################################### # test next() behavior ###################################### it = result_iterable.__iter__() def invokeNext(): return next(it) # validate that invocations of next() produces the same results as expected_ordered_ids for i in xrange(len(expected_ordered_ids)): item = invokeNext() self.assertEqual(item['id'], expected_ordered_ids[i]) # after the result set is exhausted, invoking next must raise a StopIteration exception self.assertRaises(StopIteration, invokeNext) ###################################### # test fetch_next_block() behavior ###################################### results = {} cnt = 0 while True: fetched_res = result_iterable.fetch_next_block() fetched_size = len(fetched_res) for item in fetched_res: self.assertEqual(item['id'], expected_ordered_ids[cnt]) results[cnt] = item cnt = cnt + 1 if (cnt < len(expected_ordered_ids)): self.assertEqual(fetched_size, page_size, "page size") else: if cnt == len(expected_ordered_ids): self.assertTrue(fetched_size <= page_size, "last page size") break else: #cnt > expected_number_of_results self.fail("more results than expected") # validate the number of collected results self.assertEqual(len(results), len(expected_ordered_ids)) # no more results will be returned self.assertEqual(result_iterable.fetch_next_block(), []) @classmethod def create_collection(self, client, created_db): collection_definition = { 'id': 'orderby_tests collection ' + str(uuid.uuid4()), 'indexingPolicy':{ 'includedPaths':[ { 'path':'/', 'indexes':[ { 'kind':'Range', 'dataType':'Number' }, { 'kind':'Range', 'dataType':'String' } ] } ] }, 'partitionKey':{ 'paths':[ '/id' ], 'kind':documents.PartitionKind.Hash } } collection_options = { 'offerThroughput': 30000 } created_collection = client.CreateContainer(self.GetDatabaseLink(created_db), collection_definition, collection_options) return created_collection @classmethod def insert_doc(cls): # create a document using the document definition created_docs = [] for d in cls.document_definitions: created_doc = cls.client.CreateItem(cls.collection_link, d) created_docs.append(created_doc) return created_docs @classmethod def GetDatabaseLink(cls, database, is_name_based=True): if is_name_based: return 'dbs/' + database['id'] else: return database['_self'] @classmethod def GetDocumentCollectionLink(cls, database, document_collection, is_name_based=True): if is_name_based: return cls.GetDatabaseLink(database) + '/colls/' + document_collection['id'] else: return document_collection['_self'] @classmethod def GetDocumentLink(cls, database, document_collection, document, is_name_based=True): if is_name_based: return cls.GetDocumentCollectionLink(database, document_collection) + '/docs/' + document['id'] else: return document['_self'] if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()azure-cosmos-python-3.1.1/test/proxy_tests.py000066400000000000000000000100671352206500100214140ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import pytest import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client import test.test_config as test_config import six if six.PY2: from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer else: from http.server import BaseHTTPRequestHandler, HTTPServer from threading import Thread from requests.exceptions import ProxyError @pytest.mark.usefixtures("teardown") class CustomRequestHandler(BaseHTTPRequestHandler): database_name = None def _set_headers(self): self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() def _send_payload(self): self._set_headers() payload = "{\"id\":\"" + self.database_name + "\", \"_self\":\"self_link\"}" if six.PY2: self.wfile.write(payload) else: self.wfile.write(bytes(payload, "utf-8")) def do_GET(self): self._send_payload() def do_POST(self): self._send_payload() class Server(Thread): def __init__(self, database_name, PORT): Thread.__init__(self) server_address = ('', PORT) CustomRequestHandler.database_name = database_name self.httpd = HTTPServer(server_address, CustomRequestHandler) def run(self): self.httpd.serve_forever() def shutdown(self): self.httpd.shutdown() class ProxyTests(unittest.TestCase): """Proxy Tests. """ host = 'http://localhost:8081' masterKey = test_config._test_config.masterKey testDbName = 'sample database' serverPort = 8089 @classmethod def setUpClass(cls): global server global connection_policy server = Server(cls.testDbName, cls.serverPort) server.start() connection_policy = documents.ConnectionPolicy() connection_policy.ProxyConfiguration = documents.ProxyConfiguration() connection_policy.ProxyConfiguration.Host = 'http://127.0.0.1' @classmethod def tearDownClass(cls): server.shutdown() def test_success_with_correct_proxy(self): connection_policy.ProxyConfiguration.Port = self.serverPort client = cosmos_client.CosmosClient(self.host, {'masterKey': self.masterKey}, connection_policy) created_db = client.CreateDatabase({ 'id': self.testDbName }) self.assertEqual(created_db['id'], self.testDbName, msg="Database id is incorrect") def test_failure_with_wrong_proxy(self): connection_policy.ProxyConfiguration.Port = self.serverPort + 1 try: # client does a getDatabaseAccount on initialization, which fails client = cosmos_client.CosmosClient(self.host, {'masterKey': self.masterKey}, connection_policy) self.fail("Client instantiation is not expected") except Exception as e: self.assertTrue(type(e) is ProxyError, msg="Error is not a ProxyError") if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main() azure-cosmos-python-3.1.1/test/query_execution_context_tests.py000066400000000000000000000233641352206500100252330ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import uuid import pytest from six.moves import xrange import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client from azure.cosmos.execution_context import base_execution_context as base_execution_context import azure.cosmos.base as base import test.test_config as test_config #IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos account. # Collections are billing entities. By running these test cases, you may incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class QueryExecutionContextEndToEndTests(unittest.TestCase): """Routing Map Functionalities end to end Tests. """ host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy @classmethod def setUpClass(cls): if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_ENDPOINT_HERE]'): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") cls.client = cosmos_client.CosmosClient(QueryExecutionContextEndToEndTests.host, {'masterKey': QueryExecutionContextEndToEndTests.masterKey}, QueryExecutionContextEndToEndTests.connectionPolicy) cls.created_db = test_config._test_config.create_database_if_not_exist(cls.client) cls.created_collection = cls.create_collection(cls.client, cls.created_db) cls.collection_link = cls.created_collection['_self'] cls.document_definitions = [] # create a document using the document definition for i in xrange(20): d = {'id' : str(i), 'name': 'sample document', 'spam': 'eggs' + str(i), 'key': 'value'} cls.document_definitions.append(d) cls.insert_doc(cls.client, cls.created_db, cls.collection_link, cls.document_definitions) @classmethod def tearDownClass(cls): cls.client.DeleteContainer(cls.collection_link) def setUp(self): # sanity check: partition_key_ranges = list(self.client._ReadPartitionKeyRanges(self.collection_link)) self.assertGreaterEqual(len(partition_key_ranges), 1) # sanity check: read documents after creation queried_docs = list(self.client.ReadItems(self.collection_link)) self.assertEqual( len(queried_docs), len(self.document_definitions), 'create should increase the number of documents') def test_no_query_default_execution_context(self): options = {} options['maxItemCount'] = 2 self._test_default_execution_context(options, None, 20) def test_no_query_default_execution_context_with_small_last_page(self): options = {} options['maxItemCount'] = 3 self._test_default_execution_context(options, None, 20) def test_simple_query_default_execution_context(self): query = { 'query': 'SELECT * FROM root r WHERE r.id != @id', 'parameters': [ { 'name': '@id', 'value': '5'} ] } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 2 res = self.client.QueryItems(self.collection_link, query, options) self.assertEqual(len(list(res)), 19) self._test_default_execution_context(options, query, 19) def test_simple_query_default_execution_context_with_small_last_page(self): query = { 'query': 'SELECT * FROM root r WHERE r.id != @id', 'parameters': [ { 'name': '@id', 'value': '5'} ] } options = {} options['enableCrossPartitionQuery'] = True options['maxItemCount'] = 3 self._test_default_execution_context(options, query, 19) def _test_default_execution_context(self, options, query, expected_number_of_results): page_size = options['maxItemCount'] collection_link = self.GetDocumentCollectionLink(self.created_db, self.created_collection) path = base.GetPathFromLink(collection_link, 'docs') collection_id = base.GetResourceIdOrFullNameFromLink(collection_link) def fetch_fn(options): return self.client.QueryFeed(path, collection_id, query, options) ###################################### # test next() behavior ###################################### ex = base_execution_context._DefaultQueryExecutionContext(self.client, options, fetch_fn) it = ex.__iter__() def invokeNext(): return next(it) results = {} # validate that invocations of next() produces the same results as expected for _ in xrange(expected_number_of_results): item = invokeNext() results[item['id']] = item self.assertEqual(len(results), expected_number_of_results) # after the result set is exhausted, invoking next must raise a StopIteration exception self.assertRaises(StopIteration, invokeNext) ###################################### # test fetch_next_block() behavior ###################################### ex = base_execution_context._DefaultQueryExecutionContext(self.client, options, fetch_fn) results = {} cnt = 0 while True: fetched_res = ex.fetch_next_block() fetched_size = len(fetched_res) for item in fetched_res: results[item['id']] = item cnt += fetched_size if (cnt < expected_number_of_results): # backend may not necessarily return exactly page_size of results self.assertEqual(fetched_size, page_size, "page size") else: if cnt == expected_number_of_results: self.assertTrue(fetched_size <= page_size, "last page size") break else: #cnt > expected_number_of_results self.fail("more results than expected") # validate the number of collected results self.assertEqual(len(results), expected_number_of_results) # no more results will be returned self.assertEqual(ex.fetch_next_block(), []) @classmethod def create_collection(cls, client, created_db): collection_definition = { 'id': 'query_execution_context_tests collection ' + str(uuid.uuid4()), 'partitionKey': { 'paths': ['/id'], 'kind': documents.PartitionKind.Hash } } collection_options = { } created_collection = client.CreateContainer(created_db['_self'], collection_definition, collection_options) return created_collection @classmethod def insert_doc(cls, client, created_db, collection_link, document_definitions): # create a document using the document definition created_docs = [] for d in document_definitions: created_doc = client.CreateItem(collection_link, d) created_docs.append(created_doc) return created_docs def GetDatabaseLink(self, database, is_name_based=True): if is_name_based: return 'dbs/' + database['id'] else: return database['_self'] def GetDocumentCollectionLink(self, database, document_collection, is_name_based=True): if is_name_based: return self.GetDatabaseLink(database) + '/colls/' + document_collection['id'] else: return document_collection['_self'] if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()azure-cosmos-python-3.1.1/test/query_tests.py000066400000000000000000000222131352206500100213740ustar00rootroot00000000000000import unittest import uuid import pytest import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.retry_utility as retry_utility import test.test_config as test_config @pytest.mark.usefixtures("teardown") class QueryTest(unittest.TestCase): """Test to ensure escaping of non-ascii characters from partition key""" config = test_config._test_config host = config.host masterKey = config.masterKey connectionPolicy = config.connectionPolicy client = cosmos_client.CosmosClient(host, {'masterKey': masterKey}, connectionPolicy) created_db = config.create_database_if_not_exist(client) def test_first_and_last_slashes_trimmed_for_query_string (self): created_collection = self.config.create_multi_partition_collection_with_custom_pk_if_not_exist(self.client) document_definition = {'pk': 'pk', 'id':'myId'} self.client.CreateItem(created_collection['_self'], document_definition) query_options = {'partitionKey': 'pk'} collectionLink = '/dbs/' + self.created_db['id'] + '/colls/' + created_collection['id'] + '/' query = 'SELECT * from c' query_iterable = self.client.QueryItems(collectionLink, query, query_options) iter_list = list(query_iterable) self.assertEqual(iter_list[0]['id'], 'myId') def test_query_change_feed(self): created_collection = self.config.create_multi_partition_collection_with_custom_pk_if_not_exist(self.client) collection_link = created_collection['_self'] # The test targets partition #3 pkRangeId = "3" # Read change feed with passing options query_iterable = self.client.QueryItemsChangeFeed(collection_link) iter_list = list(query_iterable) self.assertEqual(len(iter_list), 0) # Read change feed without specifying partition key range ID options = {} query_iterable = self.client.QueryItemsChangeFeed(collection_link, options) iter_list = list(query_iterable) self.assertEqual(len(iter_list), 0) # Read change feed from current should return an empty list options['partitionKeyRangeId'] = pkRangeId query_iterable = self.client.QueryItemsChangeFeed(collection_link, options) iter_list = list(query_iterable) self.assertEqual(len(iter_list), 0) self.assertTrue('etag' in self.client.last_response_headers) self.assertNotEquals(self.client.last_response_headers['etag'], '') # Read change feed from beginning should return an empty list options['isStartFromBeginning'] = True query_iterable = self.client.QueryItemsChangeFeed(collection_link, options) iter_list = list(query_iterable) self.assertEqual(len(iter_list), 0) self.assertTrue('etag' in self.client.last_response_headers) continuation1 = self.client.last_response_headers['etag'] self.assertNotEquals(continuation1, '') # Create a document. Read change feed should return be able to read that document document_definition = {'pk': 'pk', 'id':'doc1'} self.client.CreateItem(collection_link, document_definition) query_iterable = self.client.QueryItemsChangeFeed(collection_link, options) iter_list = list(query_iterable) self.assertEqual(len(iter_list), 1) self.assertEqual(iter_list[0]['id'], 'doc1') self.assertTrue('etag' in self.client.last_response_headers) continuation2 = self.client.last_response_headers['etag'] self.assertNotEquals(continuation2, '') self.assertNotEquals(continuation2, continuation1) # Create two new documents. Verify that change feed contains the 2 new documents # with page size 1 and page size 100 document_definition = {'pk': 'pk', 'id':'doc2'} self.client.CreateItem(collection_link, document_definition) document_definition = {'pk': 'pk', 'id':'doc3'} self.client.CreateItem(collection_link, document_definition) options['isStartFromBeginning'] = False for pageSize in [1, 100]: # verify iterator options['continuation'] = continuation2 options['maxItemCount'] = pageSize query_iterable = self.client.QueryItemsChangeFeed(collection_link, options) it = query_iterable.__iter__() expected_ids = 'doc2.doc3.' actual_ids = '' for item in it: actual_ids += item['id'] + '.' self.assertEqual(actual_ids, expected_ids) # verify fetch_next_block # the options is not copied, therefore it need to be restored options['continuation'] = continuation2 query_iterable = self.client.QueryItemsChangeFeed(collection_link, options) count = 0 expected_count = 2 all_fetched_res = [] while (True): fetched_res = query_iterable.fetch_next_block() self.assertEquals(len(fetched_res), min(pageSize, expected_count - count)) count += len(fetched_res) all_fetched_res.extend(fetched_res) if len(fetched_res) == 0: break actual_ids = '' for item in all_fetched_res: actual_ids += item['id'] + '.' self.assertEqual(actual_ids, expected_ids) # verify there's no more results self.assertEquals(query_iterable.fetch_next_block(), []) # verify reading change feed from the beginning options['isStartFromBeginning'] = True options['continuation'] = None query_iterable = self.client.QueryItemsChangeFeed(collection_link, options) expected_ids = ['doc1', 'doc2', 'doc3'] it = query_iterable.__iter__() for i in range(0, len(expected_ids)): doc = next(it) self.assertEquals(doc['id'], expected_ids[i]) self.assertTrue('etag' in self.client.last_response_headers) continuation3 = self.client.last_response_headers['etag'] # verify reading empty change feed options['continuation'] = continuation3 query_iterable = self.client.QueryItemsChangeFeed(collection_link, options) iter_list = list(query_iterable) self.assertEqual(len(iter_list), 0) def test_populate_query_metrics (self): created_collection = self.config.create_multi_partition_collection_with_custom_pk_if_not_exist(self.client) document_definition = {'pk': 'pk', 'id':'myId'} self.client.CreateItem(created_collection['_self'], document_definition) query_options = {'partitionKey': 'pk', 'populateQueryMetrics': True} query = 'SELECT * from c' query_iterable = self.client.QueryItems(created_collection['_self'], query, query_options) iter_list = list(query_iterable) self.assertEqual(iter_list[0]['id'], 'myId') METRICS_HEADER_NAME = 'x-ms-documentdb-query-metrics' self.assertTrue(METRICS_HEADER_NAME in self.client.last_response_headers) metrics_header = self.client.last_response_headers[METRICS_HEADER_NAME] # Validate header is well-formed: "key1=value1;key2=value2;etc" metrics = metrics_header.split(';') self.assertTrue(len(metrics) > 1) self.assertTrue(all(['=' in x for x in metrics])) def test_max_item_count_honored_in_order_by_query(self): created_collection = self.config.create_multi_partition_collection_with_custom_pk_if_not_exist(self.client) docs = [] for i in range(10): document_definition = {'pk': 'pk', 'id': 'myId' + str(uuid.uuid4())} docs.append(self.client.CreateItem(created_collection['_self'], document_definition)) query = 'SELECT * from c ORDER BY c._ts' query_options = {'enableCrossPartitionQuery': True, 'maxItemCount': 1} query_iterable = self.client.QueryItems(created_collection['_self'], query, query_options) #1 call to get query plans, 1 call to get pkr, 10 calls to one partion with the documents, 1 call each to other 4 partitions self.validate_query_requests_count(query_iterable, 16 * 2) query_options['maxItemCount'] = 100 query_iterable = self.client.QueryItems(created_collection['_self'], query, query_options) # 1 call to get query plan 1 calls to one partition with the documents, 1 call each to other 4 partitions self.validate_query_requests_count(query_iterable, 6 * 2) def validate_query_requests_count(self, query_iterable, expected_count): self.count = 0 self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunction block = query_iterable.fetch_next_block() while block: block = query_iterable.fetch_next_block() retry_utility._ExecuteFunction = self.OriginalExecuteFunction self.assertEquals(self.count, expected_count) self.count = 0 def _MockExecuteFunction(self, function, *args, **kwargs): self.count += 1 return self.OriginalExecuteFunction(function, *args, **kwargs) if __name__ == "__main__": unittest.main()azure-cosmos-python-3.1.1/test/retry_policy_tests.py000066400000000000000000000302671352206500100227630ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import uuid import pytest import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents import azure.cosmos.errors as errors import azure.cosmos.retry_options as retry_options from azure.cosmos.http_constants import HttpHeaders, StatusCodes, SubStatusCodes import azure.cosmos.retry_utility as retry_utility import test.test_config as test_config #IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos account. # Collections are billing entities. By running these test cases, you may incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class Test_retry_policy_tests(unittest.TestCase): host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy counter = 0 def __AssertHTTPFailureWithStatus(self, status_code, func, *args, **kwargs): """Assert HTTP failure with status. :Parameters: - `status_code`: int - `func`: function """ try: func(*args, **kwargs) self.assertFalse(True, 'function should fail.') except errors.HTTPFailure as inst: self.assertEqual(inst.status_code, status_code) @classmethod def setUpClass(cls): if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_ENDPOINT_HERE]'): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") cls.client = cosmos_client.CosmosClient(cls.host, {'masterKey': cls.masterKey}, cls.connectionPolicy) cls.created_collection = test_config._test_config.create_single_partition_collection_if_not_exist(cls.client) cls.retry_after_in_milliseconds = 1000 def test_resource_throttle_retry_policy_default_retry_after(self): connection_policy = Test_retry_policy_tests.connectionPolicy connection_policy.RetryOptions = retry_options.RetryOptions(5) client = cosmos_client.CosmosClient(Test_retry_policy_tests.host, {'masterKey': Test_retry_policy_tests.masterKey}, connection_policy) self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunction document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} try: client.CreateItem(self.created_collection['_self'], document_definition) except errors.HTTPFailure as e: self.assertEqual(e.status_code, StatusCodes.TOO_MANY_REQUESTS) self.assertEqual(connection_policy.RetryOptions.MaxRetryAttemptCount, client.last_response_headers[HttpHeaders.ThrottleRetryCount]) self.assertGreaterEqual(client.last_response_headers[HttpHeaders.ThrottleRetryWaitTimeInMs], connection_policy.RetryOptions.MaxRetryAttemptCount * self.retry_after_in_milliseconds) retry_utility._ExecuteFunction = self.OriginalExecuteFunction def test_resource_throttle_retry_policy_fixed_retry_after(self): connection_policy = Test_retry_policy_tests.connectionPolicy connection_policy.RetryOptions = retry_options.RetryOptions(5, 2000) client = cosmos_client.CosmosClient(Test_retry_policy_tests.host, {'masterKey': Test_retry_policy_tests.masterKey}, connection_policy) self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunction document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} try: client.CreateItem(self.created_collection['_self'], document_definition) except errors.HTTPFailure as e: self.assertEqual(e.status_code, StatusCodes.TOO_MANY_REQUESTS) self.assertEqual(connection_policy.RetryOptions.MaxRetryAttemptCount, client.last_response_headers[HttpHeaders.ThrottleRetryCount]) self.assertGreaterEqual(client.last_response_headers[HttpHeaders.ThrottleRetryWaitTimeInMs], connection_policy.RetryOptions.MaxRetryAttemptCount * connection_policy.RetryOptions.FixedRetryIntervalInMilliseconds) retry_utility._ExecuteFunction = self.OriginalExecuteFunction def test_resource_throttle_retry_policy_max_wait_time(self): connection_policy = Test_retry_policy_tests.connectionPolicy connection_policy.RetryOptions = retry_options.RetryOptions(5, 2000, 3) client = cosmos_client.CosmosClient(Test_retry_policy_tests.host, {'masterKey': Test_retry_policy_tests.masterKey}, connection_policy) self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunction document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} try: client.CreateItem(self.created_collection['_self'], document_definition) except errors.HTTPFailure as e: self.assertEqual(e.status_code, StatusCodes.TOO_MANY_REQUESTS) self.assertGreaterEqual(client.last_response_headers[HttpHeaders.ThrottleRetryWaitTimeInMs], connection_policy.RetryOptions.MaxWaitTimeInSeconds * 1000) retry_utility._ExecuteFunction = self.OriginalExecuteFunction def test_resource_throttle_retry_policy_query(self): connection_policy = Test_retry_policy_tests.connectionPolicy connection_policy.RetryOptions = retry_options.RetryOptions(5) client = cosmos_client.CosmosClient(Test_retry_policy_tests.host, {'masterKey': Test_retry_policy_tests.masterKey}, connection_policy) document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} client.CreateItem(self.created_collection['_self'], document_definition) self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunction try: list(client.QueryItems( self.created_collection['_self'], { 'query': 'SELECT * FROM root r WHERE r.id=@id', 'parameters': [ { 'name':'@id', 'value':document_definition['id'] } ] })) except errors.HTTPFailure as e: self.assertEqual(e.status_code, StatusCodes.TOO_MANY_REQUESTS) self.assertEqual(connection_policy.RetryOptions.MaxRetryAttemptCount, client.last_response_headers[HttpHeaders.ThrottleRetryCount]) self.assertGreaterEqual(client.last_response_headers[HttpHeaders.ThrottleRetryWaitTimeInMs], connection_policy.RetryOptions.MaxRetryAttemptCount * self.retry_after_in_milliseconds) retry_utility._ExecuteFunction = self.OriginalExecuteFunction def test_default_retry_policy_for_query(self): connection_policy = Test_retry_policy_tests.connectionPolicy client = cosmos_client.CosmosClient(Test_retry_policy_tests.host, {'masterKey': Test_retry_policy_tests.masterKey}, connection_policy) document_definition_1 = { 'id': 'doc1', 'name': 'sample document', 'key': 'value'} document_definition_2 = { 'id': 'doc2', 'name': 'sample document', 'key': 'value'} client.CreateItem(self.created_collection['_self'], document_definition_1) client.CreateItem(self.created_collection['_self'], document_definition_2) self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunctionConnectionReset docs = client.QueryItems(self.created_collection['_self'], "Select * from c", {'maxItemCount':1}) result_docs = list(docs) self.assertEqual(result_docs[0]['id'], 'doc1') self.assertEqual(result_docs[1]['id'], 'doc2') self.assertEqual(self.counter, 12) self.counter = 0 retry_utility._ExecuteFunction = self.OriginalExecuteFunction client.DeleteItem(result_docs[0]['_self']) client.DeleteItem(result_docs[1]['_self']) def test_default_retry_policy_for_read(self): connection_policy = Test_retry_policy_tests.connectionPolicy client = cosmos_client.CosmosClient(Test_retry_policy_tests.host, {'masterKey': Test_retry_policy_tests.masterKey}, connection_policy) document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} created_document = client.CreateItem(self.created_collection['_self'], document_definition) self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunctionConnectionReset doc = client.ReadItem(created_document['_self'], {}) self.assertEqual(doc['id'], 'doc') self.assertEqual(self.counter, 3) self.counter = 0 retry_utility._ExecuteFunction = self.OriginalExecuteFunction client.DeleteItem(doc['_self']) def test_default_retry_policy_for_create(self): connection_policy = Test_retry_policy_tests.connectionPolicy client = cosmos_client.CosmosClient(Test_retry_policy_tests.host, {'masterKey': Test_retry_policy_tests.masterKey}, connection_policy) document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunctionConnectionReset created_document = {} try : created_document = client.CreateItem(self.created_collection['_self'], document_definition) except errors.HTTPFailure as err: self.assertEqual(err.status_code, 10054) self.assertDictEqual(created_document, {}) # 3 retries for readCollection. No retry for createDocument. self.assertEqual(self.counter, 4) retry_utility._ExecuteFunction = self.OriginalExecuteFunction def _MockExecuteFunction(self, function, *args, **kwargs): raise errors.HTTPFailure(StatusCodes.TOO_MANY_REQUESTS, "Request rate is too large", {HttpHeaders.RetryAfterInMilliseconds: self.retry_after_in_milliseconds}) def _MockExecuteFunctionConnectionReset(self, function, *args, **kwargs): self.counter += 1 if self.counter % 3 == 0: return self.OriginalExecuteFunction(function, *args, **kwargs) else: raise errors.HTTPFailure(10054, "Connection was reset", {}) if __name__ == '__main__': unittest.main() azure-cosmos-python-3.1.1/test/routing/000077500000000000000000000000001352206500100201225ustar00rootroot00000000000000azure-cosmos-python-3.1.1/test/routing/__init__.py000066400000000000000000000021171352206500100222340ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE.azure-cosmos-python-3.1.1/test/routing/collection_routing_map_test.py000066400000000000000000000241461352206500100263010ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import pytest from azure.cosmos.routing.collection_routing_map import _CollectionRoutingMap import azure.cosmos.routing.routing_range as routing_range from azure.cosmos.routing.routing_map_provider import _PartitionKeyRangeCache @pytest.mark.usefixtures("teardown") class CollectionRoutingMapTests(unittest.TestCase): def test_advanced(self): partition_key_ranges = [{u'id': u'0', u'minInclusive': u'', u'maxExclusive': u'05C1C9CD673398'}, {u'id': u'1', u'minInclusive': u'05C1C9CD673398', u'maxExclusive': u'05C1D9CD673398'}, {u'id': u'2', u'minInclusive': u'05C1D9CD673398', u'maxExclusive': u'05C1E399CD6732'}, {u'id': u'3', u'minInclusive': u'05C1E399CD6732', u'maxExclusive': u'05C1E9CD673398'}, {u'id': u'4', u'minInclusive': u'05C1E9CD673398', u'maxExclusive': u'FF'}] partitionRangeWithInfo = [(r, True) for r in partition_key_ranges] pkRange = routing_range._Range("", "FF", True, False) collection_routing_map = _CollectionRoutingMap.CompleteRoutingMap(partitionRangeWithInfo, 'sample collection id') overlapping_partition_key_ranges = collection_routing_map.get_overlapping_ranges(pkRange) self.assertEqual(len(overlapping_partition_key_ranges), len(partition_key_ranges)) self.assertEqual(overlapping_partition_key_ranges, partition_key_ranges) def test_partition_key_ranges_parent_filter(self): # for large collection with thousands of partitions, a split may complete between the read partition key ranges query pages, # causing the return map to have both the new children ranges and their ranges. This test is to verify the fix for that. Id = 'id' MinInclusive = 'minInclusive' MaxExclusive = 'maxExclusive' Parents = 'parents' # create a complete set of partition key ranges # some have parents as empty array while some don't have the parents partitionKeyRanges = \ [ {Id : "2", MinInclusive : "0000000050", MaxExclusive : "0000000070", Parents : []}, {Id : "0", MinInclusive : "", MaxExclusive : "0000000030"}, {Id : "1", MinInclusive : "0000000030", MaxExclusive : "0000000050"}, {Id : "3", MinInclusive : "0000000070", MaxExclusive : "FF", Parents : []} ] def get_range_id(r): return r[Id] # verify no thing is filtered out since there is no children ranges filteredRanges = _PartitionKeyRangeCache._discard_parent_ranges(partitionKeyRanges) self.assertEqual(['2', '0', '1', '3'], list(map(get_range_id, filteredRanges))) # add some children partition key ranges with parents Ids # e.g., range 0 was split in to range 4 and 5, and then range 4 was split into range 6 and 7 partitionKeyRanges.append({Id : "6", MinInclusive : "", MaxExclusive : "0000000010", Parents : ["0", "4"]}) partitionKeyRanges.append({Id : "7", MinInclusive : "0000000010", MaxExclusive : "0000000020", Parents : ["0", "4"]}) partitionKeyRanges.append({Id : "5", MinInclusive : "0000000020", MaxExclusive : "0000000030", Parents : ["0"]}) # verify the filtered range list has children ranges and the parent Ids are discarded filteredRanges = _PartitionKeyRangeCache._discard_parent_ranges(partitionKeyRanges) expectedRanges = ['2', '1', '3', '6', '7', '5'] self.assertEqual(expectedRanges, list(map(get_range_id, filteredRanges))) def test_collection_routing_map(self): Id = 'id' MinInclusive = 'minInclusive' MaxExclusive = 'maxExclusive' partitionKeyRanges = \ [ ({Id : "2", MinInclusive : "0000000050", MaxExclusive : "0000000070"}, 2), ({Id : "0", MinInclusive : "", MaxExclusive : "0000000030"}, 0), ({Id : "1", MinInclusive : "0000000030", MaxExclusive : "0000000050"}, 1), ({Id : "3", MinInclusive : "0000000070", MaxExclusive : "FF"}, 3) ] crm = _CollectionRoutingMap.CompleteRoutingMap(partitionKeyRanges, "") self.assertEqual("0", crm._orderedPartitionKeyRanges[0][Id]) self.assertEqual("1", crm._orderedPartitionKeyRanges[1][Id]) self.assertEqual("2", crm._orderedPartitionKeyRanges[2][Id]) self.assertEqual("3", crm._orderedPartitionKeyRanges[3][Id]) self.assertEqual(0, crm._orderedPartitionInfo[0]) self.assertEqual(1, crm._orderedPartitionInfo[1]) self.assertEqual(2, crm._orderedPartitionInfo[2]) self.assertEqual(3, crm._orderedPartitionInfo[3]) self.assertEqual("0", crm.get_range_by_effective_partition_key("")[Id]) self.assertEqual("0", crm.get_range_by_effective_partition_key("0000000000")[Id]) self.assertEqual("1", crm.get_range_by_effective_partition_key("0000000030")[Id]) self.assertEqual("1", crm.get_range_by_effective_partition_key("0000000031")[Id]) self.assertEqual("3", crm.get_range_by_effective_partition_key("0000000071")[Id]) self.assertEqual("0", crm.get_range_by_partition_key_range_id("0")[Id]) self.assertEqual("1", crm.get_range_by_partition_key_range_id("1")[Id]) fullRangeMinToMaxRange = routing_range._Range(_CollectionRoutingMap.MinimumInclusiveEffectivePartitionKey, _CollectionRoutingMap.MaximumExclusiveEffectivePartitionKey, True, False) overlappingRanges = crm.get_overlapping_ranges([fullRangeMinToMaxRange]) self.assertEqual(4, len(overlappingRanges)) onlyParitionRanges = [item[0] for item in partitionKeyRanges] def getKey(r): return r['id'] onlyParitionRanges.sort(key = getKey) self.assertEqual(overlappingRanges, onlyParitionRanges) noPoint = routing_range._Range(_CollectionRoutingMap.MinimumInclusiveEffectivePartitionKey, _CollectionRoutingMap.MinimumInclusiveEffectivePartitionKey, False, False) self.assertEqual(0, len(crm.get_overlapping_ranges([noPoint]))) onePoint = routing_range._Range("0000000040", "0000000040", True, True) overlappingPartitionKeyRanges = crm.get_overlapping_ranges([onePoint]) self.assertEqual(1, len(overlappingPartitionKeyRanges)) self.assertEqual("1", overlappingPartitionKeyRanges[0][Id]) ranges = [ routing_range._Range("0000000040", "0000000045", True, True), routing_range._Range("0000000045", "0000000046", True, True), routing_range._Range("0000000046", "0000000050", True, True) ] overlappingPartitionKeyRanges = crm.get_overlapping_ranges(ranges) self.assertEqual(2, len(overlappingPartitionKeyRanges)) self.assertEqual("1", overlappingPartitionKeyRanges[0][Id]) self.assertEqual("2", overlappingPartitionKeyRanges[1][Id]) def test_invalid_routing_map(self): partitionKeyRanges = \ [ ({ 'id' : "1", 'minInclusive' : "0000000020", 'maxExclusive' : "0000000030"}, 2), ({ 'id' : "2", 'minInclusive' : "0000000025", 'maxExclusive' : "0000000035"}, 2), ] collectionUniqueId = "" def createRoutingMap(): _CollectionRoutingMap.CompleteRoutingMap(partitionKeyRanges, collectionUniqueId) self.assertRaises(ValueError, createRoutingMap) def test_incomplete_routing_map(self): crm = _CollectionRoutingMap.CompleteRoutingMap( [ ({ 'id' : "2", 'minInclusive' : "", 'maxExclusive' : "0000000030"}, 2), ({ 'id' : "3", 'minInclusive' : "0000000031", 'maxExclusive' : "FF"}, 2), ] , "") self.assertIsNone(crm) crm = _CollectionRoutingMap.CompleteRoutingMap( [ ({ 'id' : "2", 'minInclusive' : "", 'maxExclusive' : "0000000030"}, 2), ({ 'id' : "2", 'minInclusive' : "0000000030", 'maxExclusive' : "FF"}, 2), ] , "") self.assertIsNotNone(crm) if __name__ == '__main__': unittest.main() azure-cosmos-python-3.1.1/test/routing/routing_map_provider_tests.py000066400000000000000000000241631352206500100261620ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import pytest from azure.cosmos.routing.routing_map_provider import _SmartRoutingMapProvider from azure.cosmos.routing.routing_map_provider import _CollectionRoutingMap from azure.cosmos.routing import routing_range as routing_range @pytest.mark.usefixtures("teardown") class RoutingMapProviderTests(unittest.TestCase): class MockedCosmosClient(object): def __init__(self, partition_key_ranges): self.partition_key_ranges = partition_key_ranges def _ReadPartitionKeyRanges(self, collection_link): return self.partition_key_ranges def setUp(self): self.partition_key_ranges = [{u'id': u'0', u'minInclusive': u'', u'maxExclusive': u'05C1C9CD673398'}, {u'id': u'1', u'minInclusive': u'05C1C9CD673398', u'maxExclusive': u'05C1D9CD673398'}, {u'id': u'2', u'minInclusive': u'05C1D9CD673398', u'maxExclusive': u'05C1E399CD6732'}, {u'id': u'3', u'minInclusive': u'05C1E399CD6732', u'maxExclusive': u'05C1E9CD673398'}, {u'id': u'4', u'minInclusive': u'05C1E9CD673398', u'maxExclusive': u'FF'}] self.smart_routing_map_provider = self.instantiate_smart_routing_map_provider(self.partition_key_ranges) partitionRangeWithInfo = map(lambda r: (r, True), self.partition_key_ranges) self.cached_collection_routing_map = _CollectionRoutingMap.CompleteRoutingMap(partitionRangeWithInfo, 'sample collection id') def instantiate_smart_routing_map_provider(self, partition_key_ranges): client = RoutingMapProviderTests.MockedCosmosClient(partition_key_ranges) return _SmartRoutingMapProvider(client) def test_full_range(self): # query range is the whole partition key range pkRange = routing_range._Range("", "FF", True, False) overlapping_partition_key_ranges = self.get_overlapping_ranges([pkRange]) self.assertEqual(len(overlapping_partition_key_ranges), len(self.partition_key_ranges)) self.assertEqual(overlapping_partition_key_ranges, self.partition_key_ranges) pkRange = routing_range._Range("", "FF", False, False) overlapping_partition_key_ranges = self.get_overlapping_ranges([pkRange]) self.assertEqual(overlapping_partition_key_ranges, self.partition_key_ranges) self.assertEqual(self.cached_collection_routing_map.get_overlapping_ranges([pkRange]), self.partition_key_ranges) def test_empty_ranges(self): # query range is the whole partition key range pkRange = routing_range._Range("", "FF", True, False) overlapping_partition_key_ranges = self.get_overlapping_ranges([pkRange]) self.assertEqual(len(overlapping_partition_key_ranges), len(self.partition_key_ranges)) self.assertEqual(overlapping_partition_key_ranges, self.partition_key_ranges) # query range list is empty overlapping_partition_key_ranges = self.get_overlapping_ranges([]) self.assertEqual(len(overlapping_partition_key_ranges), 0) # validate the overlaping partition key ranges results for empty ranges is empty empty_start_range = routing_range._Range("", "", False, True) empty_end_range = routing_range._Range("FF", "FF", False, True) empty_range = routing_range._Range("AA", "AA", False, True) self.validate_empty_query_ranges([empty_range], [empty_start_range], [empty_end_range], [empty_start_range, empty_range], [empty_start_range, empty_end_range], [empty_range, empty_end_range], [empty_range, empty_range, empty_end_range]) def test_bad_overlapping_query_ranges(self): # they share AA point r1 = routing_range._Range("", "AA", True, True) r2 = routing_range._Range("AA", "FF", True, False) def func_one_point_overlap(): self.smart_routing_map_provider.get_overlapping_ranges("sample collection id", [r1, r2]) self.assertRaises(ValueError, func_one_point_overlap) # overlapping range r1 = routing_range._Range("", "AB", True, False) r2 = routing_range._Range("AA", "FA", True, False) def func_overlap(): self.smart_routing_map_provider.get_overlapping_ranges("sample collection id", [r1, r2]) self.assertRaises(ValueError, func_overlap) r1 = routing_range._Range("AB", "AC", True, False) r1 = routing_range._Range("AA", "AB", True, False) def func_non_sorted(): self.smart_routing_map_provider.get_overlapping_ranges("sample collection id", [r1, r2]) self.assertRaises(ValueError, func_overlap) def test_empty_ranges_are_thrown_away(self): e1 = routing_range._Range("", "", True, False) r1 = routing_range._Range("", "AB", True, False) e2 = routing_range._Range("AB", "AB", True, False) r2 = routing_range._Range("AB", "AC", True, False) e3 = routing_range._Range("AC", "AC", True, False) e4 = routing_range._Range("AD", "AD", True, False) self.validate_overlapping_ranges_results([e1, r1, e2, r2, e3, e4], self.get_overlapping_ranges([r1, r2])) self.validate_against_cached_collection_results([e1, r1, e2, r2, e3, e4]) def test_simple(self): r = routing_range._Range("AB", "AC", True, False) self.validate_against_cached_collection_results([r]) ranges = [ routing_range._Range("0000000040", "0000000045", True, False), routing_range._Range("0000000045", "0000000046", True, False), routing_range._Range("0000000046", "0000000050", True, False) ] self.validate_against_cached_collection_results(ranges) def test_simple_boundary(self): ranges = [ routing_range._Range("05C1C9CD673398", "05C1D9CD673398", True, False), ] self.validate_against_cached_collection_results(ranges) self.validate_overlapping_ranges_results(ranges, self.partition_key_ranges[1:2]) def test_two_adjacent_boundary(self): ranges = [ # self.partition_key_ranges[1] routing_range._Range("05C1C9CD673398", "05C1D9CD673398", True, False), # self.partition_key_ranges[2] routing_range._Range("05C1D9CD673398", "05C1D9CD673399", True, False), ] self.validate_against_cached_collection_results(ranges) self.validate_overlapping_ranges_results(ranges, self.partition_key_ranges[1:3]) def test_two_ranges_in_one_partition_key_range(self): # two ranges fall in the same partition key range ranges = [ routing_range._Range("05C1C9CD673400", "05C1C9CD673401", True, False), routing_range._Range("05C1C9CD673402", "05C1C9CD673403", True, False), ] self.validate_against_cached_collection_results(ranges) self.validate_overlapping_ranges_results(ranges, self.partition_key_ranges[1:2]) def test_complex(self): ranges = [ # all are covered by self.partition_key_ranges[1] routing_range._Range("05C1C9CD673398", "05C1D9CD673391", True, False), routing_range._Range("05C1D9CD673391", "05C1D9CD673392", True, False), routing_range._Range("05C1D9CD673393", "05C1D9CD673395", True, False), routing_range._Range("05C1D9CD673395", "05C1D9CD673395", True, False), # all are covered by self.partition_key_ranges[4]] routing_range._Range("05C1E9CD673398", "05C1E9CD673401", True, False), routing_range._Range("05C1E9CD673402", "05C1E9CD673403", True, False), # empty range routing_range._Range("FF", "FF", True, False), ] self.validate_against_cached_collection_results(ranges) self.validate_overlapping_ranges_results(ranges, [self.partition_key_ranges[1], self.partition_key_ranges[4]]) def validate_against_cached_collection_results(self, queryRanges): # validates the results of smart routing map provider against the results of cached colleciton map overlapping_partition_key_ranges = self.get_overlapping_ranges(queryRanges) self.assertEqual(overlapping_partition_key_ranges, self.cached_collection_routing_map.get_overlapping_ranges(queryRanges)) def validate_overlapping_ranges_results(self, queryRanges, expected_overlapping_partition_key_ranges): overlapping_partition_key_ranges = self.get_overlapping_ranges(queryRanges) self.assertEqual(overlapping_partition_key_ranges, expected_overlapping_partition_key_ranges) def validate_empty_query_ranges(self, smart_routing_map_provider, *queryRangesList): for queryRanges in queryRangesList: self.validate_overlapping_ranges_results(queryRanges, []) def get_overlapping_ranges(self, queryRanges): return self.smart_routing_map_provider.get_overlapping_ranges("sample collection id", queryRanges) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()azure-cosmos-python-3.1.1/test/routing_map_tests.py000066400000000000000000000067301352206500100225610ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import pytest import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client from azure.cosmos.routing.routing_map_provider import _PartitionKeyRangeCache from azure.cosmos.routing import routing_range as routing_range import test.test_config as test_config #IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos account. # Collections are billing entities. By running these test cases, you may incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class RoutingMapEndToEndTests(unittest.TestCase): """Routing Map Functionalities end to end Tests. """ host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy client = cosmos_client.CosmosClient(host, {'masterKey': masterKey}, connectionPolicy) collection_link = test_config._test_config.create_multi_partition_collection_with_custom_pk_if_not_exist(client)['_self'] @classmethod def setUpClass(cls): if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_ENDPOINT_HERE]'): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") def test_read_partition_key_ranges(self): partition_key_ranges = list(self.client._ReadPartitionKeyRanges(self.collection_link)) #"the number of expected partition ranges returned from the emulator is 5." self.assertEqual(5, len(partition_key_ranges)) def test_routing_map_provider(self): partition_key_ranges = list(self.client._ReadPartitionKeyRanges(self.collection_link)) routing_mp = _PartitionKeyRangeCache(self.client) overlapping_partition_key_ranges = routing_mp.get_overlapping_ranges(self.collection_link, routing_range._Range("", "FF", True, False)) self.assertEqual(len(overlapping_partition_key_ranges), len(partition_key_ranges)) self.assertEqual(overlapping_partition_key_ranges, partition_key_ranges) if __name__ == "__main__": #import sys;sys.argv = ['', 'Test.testName'] unittest.main()azure-cosmos-python-3.1.1/test/ru_per_min_tests.py000066400000000000000000000130321352206500100223650ustar00rootroot00000000000000# The MIT License (MIT) # Copyright (c) 2017 Microsoft Corporation # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import unittest import uuid import pytest import azure.cosmos.documents as documents import azure.cosmos.cosmos_client as cosmos_client from azure.cosmos import query_iterable import azure.cosmos.base as base import test.test_config as test_config # IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos # account. # Collections are billing entities. By running these test cases, you may # incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with # values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class RuPerMinTests(unittest.TestCase): """RuPerMinTests Tests. """ host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy client = cosmos_client.CosmosClient(host, {'masterKey': masterKey}, connectionPolicy) created_db = test_config._test_config.create_database_if_not_exist(client) @classmethod def setUpClass(cls): # creates the database, collection, and insert all the documents # we will gain some speed up in running the tests by creating the # database, collection and inserting all the docs only once if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_ENDPOINT_HERE]'): raise Exception("You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") def _query_offers(self, collection_self_link): offers = list(self.client.ReadOffers()) for o in offers: if o['resource'] == collection_self_link: return o return None def test_create_collection_with_ru_pm(self): # create an ru pm collection collection_definition = { 'id' : "test_create_collection_with_ru_pm collection" + str(uuid.uuid4()) } options = { 'offerEnableRUPerMinuteThroughput': True, 'offerVersion': "V2", 'offerThroughput': 400 } created_collection = self.client.CreateContainer(self.created_db['_self'], collection_definition, options) offer = self._query_offers(created_collection['_self']) self.assertIsNotNone(offer) self.assertEqual(offer['offerType'], "Invalid") self.assertIsNotNone(offer['content']) self.client.DeleteContainer(created_collection['_self']) def test_create_collection_without_ru_pm(self): # create a non ru pm collection collection_definition = { 'id' : "test_create_collection_without_ru_pm collection" + str(uuid.uuid4()) } options = { 'offerEnableRUPerMinuteThroughput': False, 'offerVersion': "V2", 'offerThroughput': 400 } created_collection = self.client.CreateContainer(self.created_db['_self'], collection_definition, options) offer = self._query_offers(created_collection['_self']) self.assertIsNotNone(offer) self.assertEqual(offer['offerType'], "Invalid") self.assertIsNotNone(offer['content']) self.client.DeleteContainer(created_collection['_self']) def test_create_collection_disable_ru_pm_on_request(self): # create a non ru pm collection collection_definition = { 'id' : "test_create_collection_disable_ru_pm_on_request collection" + str(uuid.uuid4()) } options = { 'offerVersion': "V2", 'offerThroughput': 400 } created_collection = self.client.CreateContainer(self.created_db['_self'], collection_definition, options) offer = self._query_offers(created_collection['_self']) self.assertIsNotNone(offer) self.assertEqual(offer['offerType'], "Invalid") self.assertIsNotNone(offer['content']) self.assertEqual(offer['content']['offerIsRUPerMinuteThroughputEnabled'], False) request_options = { 'disableRUPerMinuteUsage': True } doc = { 'id' : 'test_doc' + str(uuid.uuid4()) } self.client.CreateItem(created_collection['_self'], doc, request_options) self.client.DeleteContainer(created_collection['_self']) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() azure-cosmos-python-3.1.1/test/session_container_tests.py000066400000000000000000000077041352206500100237640ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import time # from types import * import pytest import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents import azure.cosmos.errors as errors import azure.cosmos.base as base import azure.cosmos.http_constants as http_constants import azure.cosmos.constants as constants import azure.cosmos.session as session import test.test_config as test_config @pytest.mark.usefixtures("teardown") class Test_session_container(unittest.TestCase): # this test doesn't need real credentials, or connection to server host = test_config._test_config.host masterkey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy def setUp(self): self.client = cosmos_client.CosmosClient(self.host, {'masterKey': self.masterkey}, self.connectionPolicy) self.session = self.client.Session def tearDown(self): pass def test_create_collection(self): #validate session token population after create collection request session_token = self.session.get_session_token('') assert session_token == '' create_collection_response_result = {u'_self': u'dbs/DdAkAA==/colls/DdAkAPS2rAA=/', u'_rid': u'DdAkAPS2rAA=', u'id': u'sample collection'} create_collection_response_header = {'x-ms-session-token': '0:0#409#24=-1#12=-1', 'x-ms-alt-content-path': 'dbs/sample%20database'} self.session.update_session(create_collection_response_result, create_collection_response_header) token = self.session.get_session_token(u'/dbs/sample%20database/colls/sample%20collection') assert token == '0:0#409#24=-1#12=-1' token = self.session.get_session_token(u'dbs/DdAkAA==/colls/DdAkAPS2rAA=/') assert token == '0:0#409#24=-1#12=-1' return True def test_document_requests(self): # validate session token for rid based requests create_document_response_result = {u'_self': u'dbs/DdAkAA==/colls/DdAkAPS2rAA=/docs/DdAkAPS2rAACAAAAAAAAAA==/', u'_rid': u'DdAkAPS2rAACAAAAAAAAAA==', u'id': u'eb391181-5c49-415a-ab27-848ce21d5d11'} create_document_response_header = {'x-ms-session-token': '0:0#406#24=-1#12=-1', 'x-ms-alt-content-path': 'dbs/sample%20database/colls/sample%20collection', 'x-ms-content-path': 'DdAkAPS2rAA='} self.session.update_session(create_document_response_result, create_document_response_header) token = self.session.get_session_token(u'dbs/DdAkAA==/colls/DdAkAPS2rAA=/docs/DdAkAPS2rAACAAAAAAAAAA==/') assert token == '0:0#406#24=-1#12=-1' token = self.session.get_session_token(u'dbs/sample%20database/colls/sample%20collection/docs/eb391181-5c49-415a-ab27-848ce21d5d11') assert token == '0:0#406#24=-1#12=-1' if __name__ == '__main__': unittest.main()azure-cosmos-python-3.1.1/test/session_tests.py000066400000000000000000000104271352206500100217160ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unittest import uuid import pytest from azure.cosmos.http_constants import HttpHeaders import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents import test.test_config as test_config import azure.cosmos.errors as errors from azure.cosmos.http_constants import StatusCodes, SubStatusCodes, HttpHeaders import azure.cosmos.synchronized_request as synchronized_request import azure.cosmos.retry_utility as retry_utility @pytest.mark.usefixtures("teardown") class SessionTests(unittest.TestCase): """Test to ensure escaping of non-ascii characters from partition key""" host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy client = cosmos_client.CosmosClient(host, {'masterKey': masterKey}, connectionPolicy) created_collection = test_config._test_config.create_multi_partition_collection_with_custom_pk_if_not_exist(client) def _MockRequest(self, global_endpoint_manager, request, connection_policy, requests_session, path, request_options, request_body): if HttpHeaders.SessionToken in request_options['headers']: self.last_session_token_sent = request_options['headers'][HttpHeaders.SessionToken] else: self.last_session_token_sent = None return self._OriginalRequest(global_endpoint_manager, request, connection_policy, requests_session, path, request_options, request_body) def test_session_token_not_sent_for_master_resource_ops (self): self._OriginalRequest = synchronized_request._Request synchronized_request._Request = self._MockRequest created_document = self.client.CreateItem(self.created_collection['_self'], {'id': '1' + str(uuid.uuid4()), 'pk': 'mypk'}) self.client.ReadItem(created_document['_self'], {'partitionKey': 'mypk'}) self.assertNotEqual(self.last_session_token_sent, None) self.client.ReadContainer(self.created_collection['_self']) self.assertEqual(self.last_session_token_sent, None) self.client.ReadItem(created_document['_self'], {'partitionKey': 'mypk'}) self.assertNotEqual(self.last_session_token_sent, None) synchronized_request._Request = self._OriginalRequest def _MockExecuteFunctionSessionReadFailureOnce(self, function, *args, **kwargs): raise errors.HTTPFailure(StatusCodes.NOT_FOUND, "Read Session not available", {HttpHeaders.SubStatus: SubStatusCodes.READ_SESSION_NOTAVAILABLE}) def test_clear_session_token(self): created_document = self.client.CreateItem(self.created_collection['_self'], {'id': '1' + str(uuid.uuid4()), 'pk': 'mypk'}) self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunctionSessionReadFailureOnce try: self.client.ReadItem(created_document['_self']) except errors.HTTPFailure as e: self.assertEqual(self.client.session.get_session_token(self.created_collection['_self']), "") self.assertEqual(e.status_code, StatusCodes.NOT_FOUND) self.assertEqual(e.sub_status, SubStatusCodes.READ_SESSION_NOTAVAILABLE) retry_utility._ExecuteFunction = self.OriginalExecuteFunction def _MockExecuteFunctionInvalidSessionToken(self, function, *args, **kwargs): response = {'_self':'dbs/90U1AA==/colls/90U1AJ4o6iA=/docs/90U1AJ4o6iABCT0AAAAABA==/', 'id':'1'} headers = {HttpHeaders.SessionToken: '0:2', HttpHeaders.AlternateContentPath: 'dbs/testDatabase/colls/testCollection'} return (response, headers) def test_internal_server_error_raised_for_invalid_session_token_received_from_server(self): self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunctionInvalidSessionToken try: self.client.CreateItem(self.created_collection['_self'], {'id': '1' + str(uuid.uuid4()), 'pk': 'mypk'}) self.fail() except errors.HTTPFailure as e: self.assertEqual(e._http_error_message, "Could not parse the received session token: 2") self.assertEqual(e.status_code, StatusCodes.INTERNAL_SERVER_ERROR) retry_utility._ExecuteFunction = self.OriginalExecuteFunction azure-cosmos-python-3.1.1/test/session_token_unit_tests.py000066400000000000000000000076041352206500100241600ustar00rootroot00000000000000import unittest import uuid import pytest import azure.cosmos.documents as documents from azure.cosmos.vector_session_token import VectorSessionToken from azure.cosmos.errors import CosmosError @pytest.mark.usefixtures("teardown") class SessionTokenUnitTest(unittest.TestCase): """Test to ensure escaping of non-ascii characters from partition key""" def test_validate_successful_session_token_parsing(self): #valid session token session_token = "1#100#1=20#2=5#3=30" self.assertEquals(VectorSessionToken.create(session_token).convert_to_string(), "1#100#1=20#2=5#3=30") def test_validate_session_token_parsing_with_invalid_version(self): session_token = "foo#100#1=20#2=5#3=30" self.assertIsNone(VectorSessionToken.create(session_token)) def test_validate_session_token_parsing_with_invalid_global_lsn(self): session_token = "1#foo#1=20#2=5#3=30" self.assertIsNone(VectorSessionToken.create(session_token)) def test_validate_session_token_parsing_with_invalid_region_progress(self): session_token = "1#100#1=20#2=x#3=30" self.assertIsNone(VectorSessionToken.create(session_token)) def test_validate_session_token_parsing_with_invalid_format(self): session_token = "1;100#1=20#2=40" self.assertIsNone(VectorSessionToken.create(session_token)) def test_validate_session_token_parsing_from_empty_string(self): session_token = "" self.assertIsNone(VectorSessionToken.create(session_token)) def test_validate_session_token_comparison(self): #valid session token session_token1 = VectorSessionToken.create("1#100#1=20#2=5#3=30") session_token2 = VectorSessionToken.create("2#105#4=10#2=5#3=30") self.assertIsNotNone(session_token1) self.assertIsNotNone(session_token2) self.assertFalse(session_token1.equals(session_token2)) self.assertFalse(session_token2.equals(session_token1)) session_token_merged = VectorSessionToken.create("2#105#2=5#3=30#4=10") self.assertIsNotNone(session_token_merged) self.assertTrue(session_token1.merge(session_token2).equals(session_token_merged)) session_token1 = VectorSessionToken.create("1#100#1=20#2=5#3=30") session_token2 = VectorSessionToken.create("1#100#1=10#2=8#3=30") self.assertIsNotNone(session_token1) self.assertIsNotNone(session_token2) self.assertFalse(session_token1.equals(session_token2)) self.assertFalse(session_token2.equals(session_token1)) session_token_merged = VectorSessionToken.create("1#100#1=20#2=8#3=30") self.assertIsNotNone(session_token_merged) self.assertTrue(session_token_merged.equals(session_token1.merge(session_token2))) session_token1 = VectorSessionToken.create("1#100#1=20#2=5#3=30") session_token2 = VectorSessionToken.create("1#102#1=100#2=8#3=30") self.assertIsNotNone(session_token1) self.assertIsNotNone(session_token1) self.assertFalse(session_token1.equals(session_token2)) self.assertFalse(session_token2.equals(session_token1)) session_token_merged = VectorSessionToken.create("1#102#2=8#3=30#1=100") self.assertIsNotNone(session_token_merged) self.assertTrue(session_token_merged.equals(session_token1.merge(session_token2))) session_token1 = VectorSessionToken.create("1#101#1=20#2=5#3=30") session_token2 = VectorSessionToken.create("1#100#1=20#2=5#3=30#4=40") self.assertIsNotNone(session_token1) self.assertIsNotNone(session_token2) try: session_token1.merge(session_token2) self.fail("Region progress can not be different when version is same") except CosmosError as e: self.assertEquals(str(e), "Status Code: 500. Compared session tokens '1#101#1=20#2=5#3=30' and '1#100#1=20#2=5#3=30#4=40' have unexpected regions.") azure-cosmos-python-3.1.1/test/streaming_failover_test.py000066400000000000000000000171111352206500100237250ustar00rootroot00000000000000import unittest import pytest import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.documents as documents import azure.cosmos.errors as errors from requests.exceptions import ConnectionError from azure.cosmos.http_constants import HttpHeaders, StatusCodes, SubStatusCodes import azure.cosmos.retry_utility as retry_utility import azure.cosmos.endpoint_discovery_retry_policy as endpoint_discovery_retry_policy from azure.cosmos.request_object import _RequestObject import azure.cosmos.global_endpoint_manager as global_endpoint_manager import azure.cosmos.http_constants as http_constants @pytest.mark.usefixtures("teardown") class TestStreamingFailover(unittest.TestCase): DEFAULT_ENDPOINT = "https://geotest.documents.azure.com:443/" MASTER_KEY = "SomeKeyValue" WRITE_ENDPOINT1 = "https://geotest-WestUS.documents.azure.com:443/" WRITE_ENDPOINT2 = "https://geotest-CentralUS.documents.azure.com:443/" READ_ENDPOINT1 = "https://geotest-SouthCentralUS.documents.azure.com:443/" READ_ENDPOINT2 = "https://geotest-EastUS.documents.azure.com:443/" WRITE_ENDPOINT_NAME1 = "West US" WRITE_ENDPOINT_NAME2 = "Central US" READ_ENDPOINT_NAME1 = "South Central US" READ_ENDPOINT_NAME2 = "East US" preferred_regional_endpoints = [READ_ENDPOINT_NAME1, READ_ENDPOINT_NAME2] counter = 0 endpoint_sequence = [] def test_streaming_failover(self): self.OriginalExecuteFunction = retry_utility._ExecuteFunction retry_utility._ExecuteFunction = self._MockExecuteFunctionEndpointDiscover connection_policy = documents.ConnectionPolicy() connection_policy.PreferredLocations = self.preferred_regional_endpoints connection_policy.DisableSSLVerification = True self.original_get_database_account = cosmos_client.CosmosClient.GetDatabaseAccount cosmos_client.CosmosClient.GetDatabaseAccount = self.mock_get_database_account client = cosmos_client.CosmosClient(self.DEFAULT_ENDPOINT, {'masterKey': self.MASTER_KEY}, connection_policy, documents.ConsistencyLevel.Eventual) document_definition = { 'id': 'doc', 'name': 'sample document', 'key': 'value'} created_document = {} created_document = client.CreateItem("dbs/mydb/colls/mycoll", document_definition) self.assertDictEqual(created_document, {}) self.assertDictEqual(client.last_response_headers, {}) self.assertEqual(self.counter, 10) # First request is an initial read collection. # Next 8 requests hit forbidden write exceptions and the endpoint retry policy keeps # flipping the resolved endpoint between the 2 write endpoints. # The 10th request returns the actual read document. for i in range(0,8): if i % 2 == 0: self.assertEqual(self.endpoint_sequence[i], self.WRITE_ENDPOINT1) else: self.assertEqual(self.endpoint_sequence[i], self.WRITE_ENDPOINT2) cosmos_client.CosmosClient.GetDatabaseAccount = self.original_get_database_account retry_utility._ExecuteFunction = self.OriginalExecuteFunction def mock_get_database_account(self, url_connection = None): database_account = documents.DatabaseAccount() database_account._EnableMultipleWritableLocations = True database_account._WritableLocations = [ {'name': self.WRITE_ENDPOINT_NAME1, 'databaseAccountEndpoint': self.WRITE_ENDPOINT1}, {'name': self.WRITE_ENDPOINT_NAME2, 'databaseAccountEndpoint': self.WRITE_ENDPOINT2} ] database_account._ReadableLocations = [ {'name': self.READ_ENDPOINT_NAME1, 'databaseAccountEndpoint': self.READ_ENDPOINT1}, {'name': self.READ_ENDPOINT_NAME2, 'databaseAccountEndpoint': self.READ_ENDPOINT2} ] return database_account def _MockExecuteFunctionEndpointDiscover(self, function, *args, **kwargs): self.counter += 1 if self.counter >= 10 or ( len(args) > 0 and args[1].operation_type == documents._OperationType.Read): return ({}, {}) else: self.endpoint_sequence.append(args[1].location_endpoint_to_route) raise errors.HTTPFailure(StatusCodes.FORBIDDEN, "Request is not permitted in this region", {HttpHeaders.SubStatus: SubStatusCodes.WRITE_FORBIDDEN}) def test_retry_policy_does_not_mark_null_locations_unavailable(self): self.original_get_database_account = cosmos_client.CosmosClient.GetDatabaseAccount cosmos_client.CosmosClient.GetDatabaseAccount = self.mock_get_database_account client = cosmos_client.CosmosClient(self.DEFAULT_ENDPOINT, {'masterKey': self.MASTER_KEY}, None, documents.ConsistencyLevel.Eventual) endpoint_manager = global_endpoint_manager._GlobalEndpointManager(client) self.original_mark_endpoint_unavailable_for_read_function = endpoint_manager.mark_endpoint_unavailable_for_read endpoint_manager.mark_endpoint_unavailable_for_read = self._mock_mark_endpoint_unavailable_for_read self.original_mark_endpoint_unavailable_for_write_function = endpoint_manager.mark_endpoint_unavailable_for_write endpoint_manager.mark_endpoint_unavailable_for_write = self._mock_mark_endpoint_unavailable_for_write self.original_resolve_service_endpoint = endpoint_manager.resolve_service_endpoint endpoint_manager.resolve_service_endpoint = self._mock_resolve_service_endpoint # Read and write counters count the number of times the endpoint manager's # mark_endpoint_unavailable_for_read() and mark_endpoint_unavailable_for_read() # functions were called. When a 'None' location is returned by resolve_service_endpoint(), # these functions should not be called self._read_counter = 0 self._write_counter = 0 request = _RequestObject(http_constants.ResourceType.Document, documents._OperationType.Read) endpointDiscovery_retry_policy = endpoint_discovery_retry_policy._EndpointDiscoveryRetryPolicy(documents.ConnectionPolicy(), endpoint_manager, request) endpointDiscovery_retry_policy.ShouldRetry(errors.HTTPFailure(http_constants.StatusCodes.FORBIDDEN)) self.assertEqual(self._read_counter, 0) self.assertEqual(self._write_counter, 0) self._read_counter = 0 self._write_counter = 0 request = _RequestObject(http_constants.ResourceType.Document, documents._OperationType.Create) endpointDiscovery_retry_policy = endpoint_discovery_retry_policy._EndpointDiscoveryRetryPolicy(documents.ConnectionPolicy(), endpoint_manager, request) endpointDiscovery_retry_policy.ShouldRetry(errors.HTTPFailure(http_constants.StatusCodes.FORBIDDEN)) self.assertEqual(self._read_counter, 0) self.assertEqual(self._write_counter, 0) endpoint_manager.mark_endpoint_unavailable_for_read = self.original_mark_endpoint_unavailable_for_read_function endpoint_manager.mark_endpoint_unavailable_for_write = self.original_mark_endpoint_unavailable_for_write_function cosmos_client.CosmosClient.GetDatabaseAccount = self.original_get_database_account def _mock_mark_endpoint_unavailable_for_read(self, endpoint): self._read_counter += 1 self.original_mark_endpoint_unavailable_for_read_function(endpoint) def _mock_mark_endpoint_unavailable_for_write(self, endpoint): self._write_counter += 1 self.original_mark_endpoint_unavailable_for_write_function(endpoint) def _mock_resolve_service_endpoint(self, request): return None azure-cosmos-python-3.1.1/test/test_config.py000066400000000000000000000164121352206500100213150ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import os import time import uuid import azure.cosmos.documents as documents import azure.cosmos.errors as errors from azure.cosmos.http_constants import StatusCodes try: import urllib3 urllib3.disable_warnings() except: print("no urllib3") class _test_config(object): #[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Cosmos DB Emulator Key")] masterKey = os.getenv('ACCOUNT_KEY', 'C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==') host = os.getenv('ACCOUNT_HOST', 'https://localhost:443') connectionPolicy = documents.ConnectionPolicy() connectionPolicy.DisableSSLVerification = True global_host = '[YOUR_GLOBAL_ENDPOINT_HERE]' write_location_host = '[YOUR_WRITE_ENDPOINT_HERE]' read_location_host = '[YOUR_READ_ENDPOINT_HERE]' read_location2_host = '[YOUR_READ_ENDPOINT2_HERE]' global_masterKey = '[YOUR_KEY_HERE]' write_location = '[YOUR_WRITE_LOCATION_HERE]' read_location = '[YOUR_READ_LOCATION_HERE]' read_location2 = '[YOUR_READ_LOCATION2_HERE]' THROUGHPUT_FOR_5_PARTITIONS = 30000 THROUGHPUT_FOR_1_PARTITION = 400 TEST_DATABASE_ID = "Python SDK Test Database " + str(uuid.uuid4()) TEST_COLLECTION_SINGLE_PARTITION_ID = "Single Partition Test Collection" TEST_COLLECTION_MULTI_PARTITION_ID = "Multi Partition Test Collection" TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK_ID = "Multi Partition Test Collection With Custom PK" TEST_COLLECTION_MULTI_PARTITION_PARTITION_KEY = "id" TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK_PARTITION_KEY = "pk" TEST_DATABASE = None TEST_COLLECTION_SINGLE_PARTITION = None TEST_COLLECTION_MULTI_PARTITION = None TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK = None IS_MULTIMASTER_ENABLED = False @classmethod def create_database_if_not_exist(cls, client): if cls.TEST_DATABASE is not None: return cls.TEST_DATABASE cls.try_delete_database(client) cls.TEST_DATABASE = client.CreateDatabase({'id': cls.TEST_DATABASE_ID}) cls.IS_MULTIMASTER_ENABLED = client.GetDatabaseAccount()._EnableMultipleWritableLocations return cls.TEST_DATABASE @classmethod def try_delete_database(cls, client): try: client.DeleteDatabase("dbs/" + cls.TEST_DATABASE_ID) except errors.HTTPFailure as e: if e.status_code != StatusCodes.NOT_FOUND: raise e @classmethod def create_single_partition_collection_if_not_exist(cls, client): if cls.TEST_COLLECTION_SINGLE_PARTITION is None: cls.TEST_COLLECTION_SINGLE_PARTITION = cls.create_collection_with_required_throughput(client, cls.THROUGHPUT_FOR_1_PARTITION, None) cls.remove_all_documents(client, cls.TEST_COLLECTION_SINGLE_PARTITION,None) return cls.TEST_COLLECTION_SINGLE_PARTITION @classmethod def create_multi_partition_collection_if_not_exist(cls, client): if cls.TEST_COLLECTION_MULTI_PARTITION is None: cls.TEST_COLLECTION_MULTI_PARTITION = cls.create_collection_with_required_throughput(client, cls.THROUGHPUT_FOR_5_PARTITIONS, True) cls.remove_all_documents(client, cls.TEST_COLLECTION_MULTI_PARTITION, True) return cls.TEST_COLLECTION_MULTI_PARTITION @classmethod def create_multi_partition_collection_with_custom_pk_if_not_exist(cls, client): if cls.TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK is None: cls.TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK = cls.create_collection_with_required_throughput(client, cls.THROUGHPUT_FOR_5_PARTITIONS, False) cls.remove_all_documents(client, cls.TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK, False) return cls.TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK @classmethod def create_collection_with_required_throughput(cls, client, throughput, use_id_as_partition_key): database = cls.create_database_if_not_exist(client) options = {'offerThroughput': throughput} document_collection = {} if throughput == cls.THROUGHPUT_FOR_1_PARTITION: collection_id = cls.TEST_COLLECTION_SINGLE_PARTITION_ID else: if use_id_as_partition_key: collection_id = cls.TEST_COLLECTION_MULTI_PARTITION_ID partition_key = cls.TEST_COLLECTION_MULTI_PARTITION_PARTITION_KEY else: collection_id = cls.TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK_ID partition_key = cls.TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK_PARTITION_KEY document_collection['partitionKey'] = {'paths': ['/' + partition_key],'kind': 'Hash'} document_collection['id'] = collection_id document_collection = client.CreateContainer(database['_self'], document_collection, options) return document_collection @classmethod def remove_all_documents(cls, client, document_collection, use_id_as_partition_key): while True: query_iterable = client.ReadItems(document_collection['_self']) read_documents = list(query_iterable) try: for document in read_documents: options = {} if use_id_as_partition_key is not None: if use_id_as_partition_key: options['partitionKey'] = document[cls.TEST_COLLECTION_MULTI_PARTITION_PARTITION_KEY] else: if cls.TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK_PARTITION_KEY in document: options['partitionKey'] = document[cls.TEST_COLLECTION_MULTI_PARTITION_WITH_CUSTOM_PK_PARTITION_KEY] else: options['partitionKey'] = {} client.DeleteItem(document['_self'], options) if cls.IS_MULTIMASTER_ENABLED: # sleep to ensure deletes are propagated for multimaster enabled accounts time.sleep(2) break except errors.HTTPFailure as e: print("Error occurred while deleting documents:" + str(e) + " \nRetrying...")azure-cosmos-python-3.1.1/test/test_partition_resolver.py000066400000000000000000000061021352206500100237750ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. """Test Partition Resolver. """ class TestPartitionResolver(object): """This is a test PartitionResolver used for resolving the collection(s) based on the partitionKey. """ def __init__(self, collection_links): """Initializes the TestPartitionResolver with the list of collections participating in partitioning :Parameters: - `collection_links`: list, Self Links or ID based Links for the collections participating in partitioning """ self.collection_links = collection_links def ResolveForCreate(self, document): """Resolves the collection for creating the document based on the partition key :Parameters: - `document`: dict, document resource :Returns: str, collection Self link or Name based link which should handle the Create operation """ # For this TestPartitionResolver, Id property of the document is the partition key partition_key = document['id'] return self._GetDocumentCollection(self._GetHashCode(partition_key)) def ResolveForRead(self, partition_key): """Resolves the collection for reading/querying the document based on the partition key :Parameters: - `partition_key`: str, partition key :Returns: str, collection Self link(s) or Name based link(s) which should handle the Read operation """ # For Read operations, partitionKey can be None in which case we return all collection links if partition_key is None: return self.collection_links else: return [self._GetDocumentCollection(self._GetHashCode(partition_key))] # Calculates the hashCode from the partition key def _GetHashCode(self, partition_key): return int(partition_key) # Gets the Document Collection from the hash code def _GetDocumentCollection(self, hashCode): return self.collection_links[abs(hashCode) % len(self.collection_links)] azure-cosmos-python-3.1.1/test/ttl_tests.py000066400000000000000000000355111352206500100210370ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import uuid import time import pytest import azure.cosmos.cosmos_client as cosmos_client import azure.cosmos.errors as errors from azure.cosmos.http_constants import StatusCodes import test.test_config as test_config #IMPORTANT NOTES: # Most test cases in this file create collections in your Azure Cosmos account. # Collections are billing entities. By running these test cases, you may incur monetary costs on your account. # To Run the test, replace the two member fields (masterKey and host) with values # associated with your Azure Cosmos account. @pytest.mark.usefixtures("teardown") class Test_ttl_tests(unittest.TestCase): """TTL Unit Tests. """ host = test_config._test_config.host masterKey = test_config._test_config.masterKey connectionPolicy = test_config._test_config.connectionPolicy client = cosmos_client.CosmosClient(host, {'masterKey': masterKey}, connectionPolicy) created_db = test_config._test_config.create_database_if_not_exist(client) def __AssertHTTPFailureWithStatus(self, status_code, func, *args, **kwargs): """Assert HTTP failure with status. :Parameters: - `status_code`: int - `func`: function """ try: func(*args, **kwargs) self.assertFalse(True, 'function should fail.') except errors.HTTPFailure as inst: self.assertEqual(inst.status_code, status_code) @classmethod def setUpClass(cls): if (cls.masterKey == '[YOUR_KEY_HERE]' or cls.host == '[YOUR_ENDPOINT_HERE]'): raise Exception( "You must specify your Azure Cosmos account values for " "'masterKey' and 'host' at the top of this class to run the " "tests.") def test_collection_and_document_ttl_values(self): collection_definition = {'id' : 'test_collection_and_document_ttl_values1' + str(uuid.uuid4()), 'defaultTtl' : 5 } created_collection = self.client.CreateContainer(self.created_db['_self'], collection_definition) self.assertEqual(created_collection['defaultTtl'], collection_definition['defaultTtl']) collection_definition['id'] = 'test_collection_and_document_ttl_values2' + str(uuid.uuid4()) collection_definition['defaultTtl'] = None # None is an unsupported value for defaultTtl. Valid values are -1 or a non-zero positive 32-bit integer value self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateContainer, self.created_db['_self'], collection_definition) collection_definition['id'] = 'test_collection_and_document_ttl_values3' + str(uuid.uuid4()) collection_definition['defaultTtl'] = 0 # 0 is an unsupported value for defaultTtl. Valid values are -1 or a non-zero positive 32-bit integer value self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateContainer, self.created_db['_self'], collection_definition) collection_definition['id'] = 'test_collection_and_document_ttl_values4' + str(uuid.uuid4()) collection_definition['defaultTtl'] = -10 # -10 is an unsupported value for defaultTtl. Valid values are -1 or a non-zero positive 32-bit integer value self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateContainer, self.created_db['_self'], collection_definition) document_definition = { 'id': 'doc1' + str(uuid.uuid4()), 'name': 'sample document', 'key': 'value', 'ttl' : 0} # 0 is an unsupported value for ttl. Valid values are -1 or a non-zero positive 32-bit integer value self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateItem, created_collection['_self'], document_definition) document_definition['id'] = 'doc2' + str(uuid.uuid4()) document_definition['ttl'] = None # None is an unsupported value for ttl. Valid values are -1 or a non-zero positive 32-bit integer value self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateItem, created_collection['_self'], document_definition) document_definition['id'] = 'doc3' + str(uuid.uuid4()) document_definition['ttl'] = -10 # -10 is an unsupported value for ttl. Valid values are -1 or a non-zero positive 32-bit integer value self.__AssertHTTPFailureWithStatus( StatusCodes.BAD_REQUEST, self.client.CreateItem, created_collection['_self'], document_definition) self.client.DeleteContainer(created_collection['_self']) def test_document_ttl_with_positive_defaultTtl(self): collection_definition = {'id' : 'test_document_ttl_with_positive_defaultTtl collection' + str(uuid.uuid4()), 'defaultTtl' : 5 } created_collection = self.client.CreateContainer(self.created_db['_self'], collection_definition) document_definition = { 'id': 'doc1' + str(uuid.uuid4()), 'name': 'sample document', 'key': 'value'} created_document = self.client.CreateItem(created_collection['_self'], document_definition) time.sleep(7) # the created document should be gone now as it's ttl value would be same as defaultTtl value of the collection self.__AssertHTTPFailureWithStatus( StatusCodes.NOT_FOUND, self.client.ReadItem, created_document['_self']) document_definition['id'] = 'doc2' + str(uuid.uuid4()) document_definition['ttl'] = -1 created_document = self.client.CreateItem(created_collection['_self'], document_definition) time.sleep(5) # the created document should NOT be gone as it's ttl value is set to -1(never expire) which overrides the collections's defaultTtl value read_document = self.client.ReadItem(created_document['_self']) self.assertEqual(created_document['id'], read_document['id']) document_definition['id'] = 'doc3' + str(uuid.uuid4()) document_definition['ttl'] = 2 created_document = self.client.CreateItem(created_collection['_self'], document_definition) time.sleep(4) # the created document should be gone now as it's ttl value is set to 2 which overrides the collections's defaultTtl value(5) self.__AssertHTTPFailureWithStatus( StatusCodes.NOT_FOUND, self.client.ReadItem, created_document['_self']) document_definition['id'] = 'doc4' + str(uuid.uuid4()) document_definition['ttl'] = 8 created_document = self.client.CreateItem(created_collection['_self'], document_definition) time.sleep(6) # the created document should NOT be gone as it's ttl value is set to 8 which overrides the collections's defaultTtl value(5) read_document = self.client.ReadItem(created_document['_self']) self.assertEqual(created_document['id'], read_document['id']) time.sleep(4) # the created document should be gone now as we have waited for (6+4) secs which is greater than documents's ttl value of 8 self.__AssertHTTPFailureWithStatus( StatusCodes.NOT_FOUND, self.client.ReadItem, created_document['_self']) self.client.DeleteContainer(created_collection['_self']) def test_document_ttl_with_negative_one_defaultTtl(self): collection_definition = {'id' : 'test_document_ttl_with_negative_one_defaultTtl collection' + str(uuid.uuid4()), 'defaultTtl' : -1 } created_collection = self.client.CreateContainer(self.created_db['_self'], collection_definition) document_definition = { 'id': 'doc1' + str(uuid.uuid4()), 'name': 'sample document', 'key': 'value'} # the created document's ttl value would be -1 inherited from the collection's defaultTtl and this document will never expire created_document1 = self.client.CreateItem(created_collection['_self'], document_definition) # This document is also set to never expire explicitly document_definition['id'] = 'doc2' + str(uuid.uuid4()) document_definition['ttl'] = -1 created_document2 = self.client.CreateItem(created_collection['_self'], document_definition) document_definition['id'] = 'doc3' + str(uuid.uuid4()) document_definition['ttl'] = 2 created_document3 = self.client.CreateItem(created_collection['_self'], document_definition) time.sleep(4) # the created document should be gone now as it's ttl value is set to 2 which overrides the collections's defaultTtl value(-1) self.__AssertHTTPFailureWithStatus( StatusCodes.NOT_FOUND, self.client.ReadItem, created_document3['_self']) # The documents with id doc1 and doc2 will never expire read_document = self.client.ReadItem(created_document1['_self']) self.assertEqual(created_document1['id'], read_document['id']) read_document = self.client.ReadItem(created_document2['_self']) self.assertEqual(created_document2['id'], read_document['id']) self.client.DeleteContainer(created_collection['_self']) def test_document_ttl_with_no_defaultTtl(self): collection_definition = {'id' : 'test_document_ttl_with_no_defaultTtl collection' + str(uuid.uuid4()) } created_collection = self.client.CreateContainer(self.created_db['_self'], collection_definition) document_definition = { 'id': 'doc1' + str(uuid.uuid4()), 'name': 'sample document', 'key': 'value', 'ttl' : 5} created_document = self.client.CreateItem(created_collection['_self'], document_definition) time.sleep(7) # Created document still exists even after ttl time has passed since the TTL is disabled at collection level(no defaultTtl property defined) read_document = self.client.ReadItem(created_document['_self']) self.assertEqual(created_document['id'], read_document['id']) self.client.DeleteContainer(created_collection['_self']) def test_document_ttl_misc(self): collection_definition = {'id' : 'test_document_ttl_misc collection' + str(uuid.uuid4()), 'defaultTtl' : 8 } created_collection = self.client.CreateContainer(self.created_db['_self'], collection_definition) document_definition = { 'id': 'doc1' + str(uuid.uuid4()), 'name': 'sample document', 'key': 'value'} created_document = self.client.CreateItem(created_collection['_self'], document_definition) time.sleep(10) # the created document cannot be deleted since it should already be gone now self.__AssertHTTPFailureWithStatus( StatusCodes.NOT_FOUND, self.client.DeleteItem, created_document['_self']) # We can create a document with the same id after the ttl time has expired created_document = self.client.CreateItem(created_collection['_self'], document_definition) self.assertEqual(created_document['id'], document_definition['id']) time.sleep(3) # Upsert the document after 3 secs to reset the document's ttl document_definition['key'] = 'value2' upserted_docment = self.client.UpsertItem(created_collection['_self'], document_definition) time.sleep(7) # Upserted document still exists after 10 secs from document creation time(with collection's defaultTtl set to 8) since it's ttl was reset after 3 secs by upserting it read_document = self.client.ReadItem(upserted_docment['_self']) self.assertEqual(upserted_docment['id'], read_document['id']) time.sleep(3) # the upserted document should be gone now after 10 secs from the last write(upsert) of the document self.__AssertHTTPFailureWithStatus( StatusCodes.NOT_FOUND, self.client.ReadItem, upserted_docment['_self']) documents = list(self.client.QueryItems( created_collection['_self'], { 'query': 'SELECT * FROM root r' })) self.assertEqual(0, len(documents)) # Removes defaultTtl property from collection to disable ttl at collection level collection_definition.pop('defaultTtl') replaced_collection = self.client.ReplaceContainer(created_collection['_self'], collection_definition) document_definition['id'] = 'doc2' + str(uuid.uuid4()) created_document = self.client.CreateItem(replaced_collection['_self'], document_definition) time.sleep(5) # Created document still exists even after ttl time has passed since the TTL is disabled at collection level read_document = self.client.ReadItem(created_document['_self']) self.assertEqual(created_document['id'], read_document['id']) self.client.DeleteContainer(created_collection['_self']) if __name__ == '__main__': try: unittest.main() except SystemExit as inst: if inst.args[0] is True: # raised by sys.exit(True) when tests failed raise azure-cosmos-python-3.1.1/test/utils_tests.py000066400000000000000000000033211352206500100213660ustar00rootroot00000000000000#The MIT License (MIT) #Copyright (c) 2014 Microsoft Corporation #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in all #copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #SOFTWARE. import unittest import pytest import azure.cosmos.utils as utils import platform import azure.cosmos.http_constants as http_constants @pytest.mark.usefixtures("teardown") class UtilsTests(unittest.TestCase): """Utils Tests """ def test_user_agent(self): user_agent = utils._get_user_agent() expected_user_agent = "{}/{} Python/{} azure-cosmos/{}".format( platform.system(), platform.release(), platform.python_version(), http_constants.Versions.SDKVersion ) self.assertEqual(user_agent, expected_user_agent) if __name__ == "__main__": unittest.main() azure-cosmos-python-3.1.1/tox.ini000066400000000000000000000010331352206500100167640ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py27, py34 [base] deps = -rtest-requirements.txt [pytest] python_files=test/**.py [testenv] passenv = * deps = {[base]deps} changedir = {envtmpdir} commands = pytest \ --junitxml={envlogdir}/Test-junit-{envname}.xml \ --verbose {toxinidir}/test/ {posargs}