From e937082d535c4609b2b07e39b8ed46209fd097d8 Mon Sep 17 00:00:00 2001 From: TreeBen77 <77905642+TreeBen77@users.noreply.github.com> Date: Wed, 30 Nov 2022 13:06:46 +1000 Subject: [PATCH 1/9] added ordered data store support --- rblxopencloud/__init__.py | 4 +- rblxopencloud/datastore.py | 132 ++++++++++++++++++++++++++++++++++++- rblxopencloud/universe.py | 5 +- 3 files changed, 136 insertions(+), 5 deletions(-) diff --git a/rblxopencloud/__init__.py b/rblxopencloud/__init__.py index 97babb6..e7f987a 100644 --- a/rblxopencloud/__init__.py +++ b/rblxopencloud/__init__.py @@ -3,5 +3,5 @@ from .exceptions import * # don't forget to update version here and in setup.py! -VERSION = "0.4.2" -VERSION_INFO = "final" \ No newline at end of file +VERSION = "0.4.3" +VERSION_INFO = "alpha" \ No newline at end of file diff --git a/rblxopencloud/datastore.py b/rblxopencloud/datastore.py index 617b720..fed3c3a 100644 --- a/rblxopencloud/datastore.py +++ b/rblxopencloud/datastore.py @@ -7,7 +7,9 @@ "EntryInfo", "EntryVersion", "ListedEntry", - "DataStore" + "DataStore", + "SortedEntry" + "OrderedDataStore" ) class EntryInfo(): @@ -306,4 +308,130 @@ def get_version(self, key: str, version: str) -> tuple[Union[str, dict, list, in elif response.status_code == 404: raise NotFound(f"The key {key} does not exist.") elif response.status_code == 429: raise RateLimited("You're being rate limited.") elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") - else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") \ No newline at end of file + else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") + +class SortedEntry(): + def __init__(self, key: str, value: int, scope: str="global") -> None: + self.key: str = key + self.scope: str = scope + self.value: int = value + + def __eq__(self, object) -> bool: + if not isinstance(object, SortedEntry): + return NotImplemented + return self.key == object.key and self.scope == object.scope and self.value == object.value + + def __repr__(self) -> str: + return f"rblxopencloud.SortedEntry(\"{self.key}\", value=\"{self.value}\")" + +class OrderedDataStore(): + def __init__(self, name, universe, api_key, scope): + self.name = name + self.__api_key = api_key + self.scope = scope + self.universe = universe + + def __repr__(self) -> str: + return f"rblxopencloud.OrderedDataStore(\"{self.name}\", scope=\"{self.scope}\", universe={repr(self.universe)})" + + def __str__(self) -> str: + return self.name + + def sort_keys(self, descending: bool=True, limit: Union[None, int]=None) -> Iterable[SortedEntry]: + if not self.scope: + raise ValueError("") + nextcursor = "" + yields = 0 + while limit == None or yields < limit: + response = requests.get(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{base64.b64encode(self.scope.encode()).decode()}/entries", + headers={"x-api-key": self.__api_key}, params={ + "max_page_size": limit if limit and limit < 100 else 100, + "order_by": "desc" if descending else None, + "page_token": nextcursor if nextcursor else None + }) + if response.status_code == 401 or response.status_code == 403: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") + elif response.status_code == 404: raise NotFound("The datastore you're trying to access does not exist.") + elif response.status_code == 429: raise RateLimited("You're being rate limited.") + elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") + elif not response.ok: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") + + data = response.json() + for key in data["entries"]: + yields += 1 + yield SortedEntry(key["target"], key["value"], self.scope) + if limit != None and yields >= limit: break + nextcursor = data.get("lastEvaluatedKey") + if not nextcursor: break + + def get(self, key: str) -> int: + try: + if not self.scope: scope, key = key.split("/", maxsplit=1) + except(ValueError): + raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") + scope = base64.b64encode(self.scope.encode() if self.scope else scope.encode()).decode() + response = requests.get(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", + headers={"x-api-key": self.__api_key}) + + if response.status_code == 200: return int(response.json()["value"]) + elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") + elif response.status_code == 404: raise NotFound(f"The key {key} does not exist.") + elif response.status_code == 429: raise RateLimited("You're being rate limited.") + elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") + else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") + + def create(self, key: str, value: int) -> int: + try: + if not self.scope: scope, key = key.split("/", maxsplit=1) + except(ValueError): + raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") + + scope = base64.b64encode(self.scope.encode() if self.scope else scope.encode()).decode() + response = requests.post(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", + headers={"x-api-key": self.__api_key}, data=str(value)) + + if response.status_code == 200: return int(response.json()["value"]) + elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") + elif response.status_code == 404: raise NotFound(f"The key {key} does not exist.") + elif response.status_code == 429: raise RateLimited("You're being rate limited.") + elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") + else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") + + def update(self, key: str, value: int, previous_value: Union[int, None]=None) -> int: + + try: + if not self.scope: scope, key = key.split("/", maxsplit=1) + except(ValueError): + raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") + + scope = base64.b64encode(self.scope.encode() if self.scope else scope.encode()).decode() + response = requests.patch(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", + headers={"x-api-key": self.__api_key}, params={ + "eTag": str(previous_value), + "allow_missing": not previous_value + }, data=str(value)) + + if response.status_code == 200: return int(response.json()["value"]) + elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") + elif response.status_code == 404: raise NotFound(f"The key {key} does not exist.") + elif response.status_code == 429: raise RateLimited("You're being rate limited.") + elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") + else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") + + def remove(self, key: str) -> None: + try: + if not self.scope: scope, key = key.split("/", maxsplit=1) + except(ValueError): + raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") + + scope = base64.b64encode(self.scope.encode() if self.scope else scope.encode()).decode() + response = requests.delete(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", + headers={"x-api-key": self.__api_key}) + + if response.status_code == 204: return None + elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") + elif response.status_code == 404: raise NotFound(f"The key {key} does not exist.") + elif response.status_code == 429: raise RateLimited("You're being rate limited.") + elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") + else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") + +"https://apis.roblox.com/datastores/v2/universes/:universe/orderedDatastores/:datastore/scopes/:scope/entries/:entry" \ No newline at end of file diff --git a/rblxopencloud/universe.py b/rblxopencloud/universe.py index 207d171..4c0fe46 100644 --- a/rblxopencloud/universe.py +++ b/rblxopencloud/universe.py @@ -1,7 +1,7 @@ from .exceptions import * import requests, io from typing import Union -from .datastore import DataStore +from .datastore import DataStore, OrderedDataStore __all__ = ( "Universe", @@ -18,6 +18,9 @@ def __repr__(self) -> str: def get_data_store(self, name: str, scope: Union[str, None]="global") -> DataStore: """Creates a `rblx-open-cloud.DataStore` without `DataStore.created` with the provided name and scope. If `scope` is `None` then keys require to be formatted like `scope/key` and `DataStore.list_keys` will return keys from all scopes.""" return DataStore(name, self, self.__api_key, None, scope) + + def get_ordered_data_store(self, name: str, scope: Union[str, None]="global") -> OrderedDataStore: + return OrderedDataStore(name, self, self.__api_key, scope) def list_data_stores(self, prefix: str="", limit: Union[None, int]=None, scope: str="global") -> list[DataStore]: """Returns an `Iterable` of all `rblx-open-cloud.DataStore` in the Universe which includes `DataStore.created`, optionally matching a prefix. The example below would list all versions, along with their value. From 2e0240c8dcec7ce7ecd2566c21317c1571ac4cf7 Mon Sep 17 00:00:00 2001 From: TreeBen77 <77905642+TreeBen77@users.noreply.github.com> Date: Wed, 30 Nov 2022 14:34:15 +1000 Subject: [PATCH 2/9] merged create and update into set --- rblxopencloud/datastore.py | 52 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/rblxopencloud/datastore.py b/rblxopencloud/datastore.py index fed3c3a..2393b52 100644 --- a/rblxopencloud/datastore.py +++ b/rblxopencloud/datastore.py @@ -8,7 +8,7 @@ "EntryVersion", "ListedEntry", "DataStore", - "SortedEntry" + "SortedEntry", "OrderedDataStore" ) @@ -378,25 +378,9 @@ def get(self, key: str) -> int: elif response.status_code == 429: raise RateLimited("You're being rate limited.") elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") - - def create(self, key: str, value: int) -> int: - try: - if not self.scope: scope, key = key.split("/", maxsplit=1) - except(ValueError): - raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") - - scope = base64.b64encode(self.scope.encode() if self.scope else scope.encode()).decode() - response = requests.post(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", - headers={"x-api-key": self.__api_key}, data=str(value)) - - if response.status_code == 200: return int(response.json()["value"]) - elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") - elif response.status_code == 404: raise NotFound(f"The key {key} does not exist.") - elif response.status_code == 429: raise RateLimited("You're being rate limited.") - elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") - else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") - def update(self, key: str, value: int, previous_value: Union[int, None]=None) -> int: + def set(self, key: str, value: int, exclusive_create: bool=False, previous_value: Union[int, None]=None) -> int: + if previous_value and exclusive_create: raise ValueError("previous_version and exclusive_create can not both be set") try: if not self.scope: scope, key = key.split("/", maxsplit=1) @@ -404,16 +388,32 @@ def update(self, key: str, value: int, previous_value: Union[int, None]=None) -> raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") scope = base64.b64encode(self.scope.encode() if self.scope else scope.encode()).decode() - response = requests.patch(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", - headers={"x-api-key": self.__api_key}, params={ - "eTag": str(previous_value), - "allow_missing": not previous_value - }, data=str(value)) + + if not exclusive_create: + response = requests.patch(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", + headers={"x-api-key": self.__api_key}, params={ + "eTag": str(previous_value), + "allow_missing": not previous_value + }, data=str(value)) + else: + response = requests.post(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", + headers={"x-api-key": self.__api_key}, data=str(value)) + if response.status_code == 400 and response.json()["errorDetails"][0].get("datastoreErrorCode") == "EntryAlreadyExists": + raise PreconditionFailed(None, None, f"An entry already exists with the provided key and scope") + if response.status_code == 200: return int(response.json()["value"]) elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") - elif response.status_code == 404: raise NotFound(f"The key {key} does not exist.") - elif response.status_code == 429: raise RateLimited("You're being rate limited.") + elif response.status_code in [404, 409]: + if exclusive_create: + error = "An entry already exists with the provided key and scope" + elif previous_value: + error = f"The current value is not {previous_value}" + else: + error = "A Precondition Failed" + + raise PreconditionFailed(None, None, error) + if response.status_code == 429: raise RateLimited("You're being rate limited.") elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") From 6457492806b3fbe84b840bdb504181470b2b11d4 Mon Sep 17 00:00:00 2001 From: TreeBen77 <77905642+TreeBen77@users.noreply.github.com> Date: Wed, 30 Nov 2022 19:54:46 +1000 Subject: [PATCH 3/9] added filter support in list ordered data stores --- rblxopencloud/datastore.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/rblxopencloud/datastore.py b/rblxopencloud/datastore.py index 2393b52..74c451a 100644 --- a/rblxopencloud/datastore.py +++ b/rblxopencloud/datastore.py @@ -9,6 +9,7 @@ "ListedEntry", "DataStore", "SortedEntry", + "SortFilter", "OrderedDataStore" ) @@ -324,6 +325,37 @@ def __eq__(self, object) -> bool: def __repr__(self) -> str: return f"rblxopencloud.SortedEntry(\"{self.key}\", value=\"{self.value}\")" +class SortFilter(): + def __init__(self, min: Union[int, None], max: Union[int, None]) -> None: + self.min: Union[int, None] = min + self.max: Union[int, None] = max + + def __eq__(self, object) -> bool: + if not isinstance(object, SortFilter): + return NotImplemented + return self.min == object.min and self.max == object.max + + def __repr__(self) -> str: + return f"rblxopencloud.SortFilter(min=\"{self.min}\", max=\"{self.max}\")"\ + + # @classmethod + # def from_range(cls, min: Union[int, None], max: Union[int, None]): + # return cls(min=min, max=max) + + def to_str(self) -> Union[str, None]: + if self.min and self.max: + if self.min > self.max: + raise ValueError("minimum must not be greater than max.") + return f"entry >= {self.min} AND entry <= {self.max}" + + if self.min: + return f"entry >= {self.min}" + + if self.min: + return f"entry <= {self.max}" + + return None + class OrderedDataStore(): def __init__(self, name, universe, api_key, scope): self.name = name @@ -337,7 +369,7 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.name - def sort_keys(self, descending: bool=True, limit: Union[None, int]=None) -> Iterable[SortedEntry]: + def sort_keys(self, descending: bool=True, filter: Union[SortFilter, None]=None, limit: Union[None, int]=None) -> Iterable[SortedEntry]: if not self.scope: raise ValueError("") nextcursor = "" @@ -347,7 +379,8 @@ def sort_keys(self, descending: bool=True, limit: Union[None, int]=None) -> Iter headers={"x-api-key": self.__api_key}, params={ "max_page_size": limit if limit and limit < 100 else 100, "order_by": "desc" if descending else None, - "page_token": nextcursor if nextcursor else None + "page_token": nextcursor if nextcursor else None, + "filter": filter.to_str() if filter else None }) if response.status_code == 401 or response.status_code == 403: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") elif response.status_code == 404: raise NotFound("The datastore you're trying to access does not exist.") From 677024ad8cb50676741e61458bc8eb0f1e13ac65 Mon Sep 17 00:00:00 2001 From: TreeBen77 <77905642+TreeBen77@users.noreply.github.com> Date: Thu, 9 Mar 2023 18:59:34 +1000 Subject: [PATCH 4/9] updated to new ordered data store endpoints --- rblxopencloud/datastore.py | 100 ++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/rblxopencloud/datastore.py b/rblxopencloud/datastore.py index 74c451a..394f7d5 100644 --- a/rblxopencloud/datastore.py +++ b/rblxopencloud/datastore.py @@ -1,7 +1,7 @@ from .exceptions import * import requests, json, datetime from typing import Union, Iterable -import base64, hashlib +import base64, hashlib, urllib.parse __all__ = ( "EntryInfo", @@ -30,7 +30,6 @@ def __init__(self, version, deleted, content_length, created, key_created, datas self.deleted = deleted self.content_length = content_length self.created = datetime.datetime.fromisoformat((created.split("Z")[0]+"0"*6)[0:26]) - print(key_created) self.key_created = datetime.datetime.fromisoformat((key_created.split("Z")[0]+"0"*6)[0:26]) self.__datastore = datastore self.__key = key @@ -323,7 +322,7 @@ def __eq__(self, object) -> bool: return self.key == object.key and self.scope == object.scope and self.value == object.value def __repr__(self) -> str: - return f"rblxopencloud.SortedEntry(\"{self.key}\", value=\"{self.value}\")" + return f"rblxopencloud.SortedEntry(\"{self.key}\", value={self.value})" class SortFilter(): def __init__(self, min: Union[int, None], max: Union[int, None]) -> None: @@ -337,16 +336,12 @@ def __eq__(self, object) -> bool: def __repr__(self) -> str: return f"rblxopencloud.SortFilter(min=\"{self.min}\", max=\"{self.max}\")"\ - - # @classmethod - # def from_range(cls, min: Union[int, None], max: Union[int, None]): - # return cls(min=min, max=max) - + def to_str(self) -> Union[str, None]: if self.min and self.max: if self.min > self.max: raise ValueError("minimum must not be greater than max.") - return f"entry >= {self.min} AND entry <= {self.max}" + return f"entry >= {self.min} && entry <= {self.max}" if self.min: return f"entry >= {self.min}" @@ -357,25 +352,24 @@ def to_str(self) -> Union[str, None]: return None class OrderedDataStore(): - def __init__(self, name, universe, api_key, scope): + def __init__(self, name, experince, api_key, scope): self.name = name self.__api_key = api_key self.scope = scope - self.universe = universe + self.experince = experince def __repr__(self) -> str: - return f"rblxopencloud.OrderedDataStore(\"{self.name}\", scope=\"{self.scope}\", universe={repr(self.universe)})" + return f"rblxopencloud.OrderedDataStore(\"{self.name}\", scope=\"{self.scope}\", experince={repr(self.experince)})" def __str__(self) -> str: return self.name def sort_keys(self, descending: bool=True, filter: Union[SortFilter, None]=None, limit: Union[None, int]=None) -> Iterable[SortedEntry]: - if not self.scope: - raise ValueError("") + if not self.scope: raise ValueError("A scope is required to list keys on OrderedDataStore.") nextcursor = "" yields = 0 while limit == None or yields < limit: - response = requests.get(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{base64.b64encode(self.scope.encode()).decode()}/entries", + response = requests.get(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDataStores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(self.scope)}/entries", headers={"x-api-key": self.__api_key}, params={ "max_page_size": limit if limit and limit < 100 else 100, "order_by": "desc" if descending else None, @@ -391,7 +385,7 @@ def sort_keys(self, descending: bool=True, filter: Union[SortFilter, None]=None, data = response.json() for key in data["entries"]: yields += 1 - yield SortedEntry(key["target"], key["value"], self.scope) + yield SortedEntry(key["path"], key["value"], self.scope) if limit != None and yields >= limit: break nextcursor = data.get("lastEvaluatedKey") if not nextcursor: break @@ -399,10 +393,10 @@ def sort_keys(self, descending: bool=True, filter: Union[SortFilter, None]=None, def get(self, key: str) -> int: try: if not self.scope: scope, key = key.split("/", maxsplit=1) - except(ValueError): - raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") - scope = base64.b64encode(self.scope.encode() if self.scope else scope.encode()).decode() - response = requests.get(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", + else: scope = self.scope + except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") + + response = requests.get(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDataStores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", headers={"x-api-key": self.__api_key}) if response.status_code == 200: return int(response.json()["value"]) @@ -412,27 +406,25 @@ def get(self, key: str) -> int: elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") - def set(self, key: str, value: int, exclusive_create: bool=False, previous_value: Union[int, None]=None) -> int: - if previous_value and exclusive_create: raise ValueError("previous_version and exclusive_create can not both be set") - + def set(self, key: str, value: int, exclusive_create: bool=False) -> int: try: if not self.scope: scope, key = key.split("/", maxsplit=1) - except(ValueError): - raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") - - scope = base64.b64encode(self.scope.encode() if self.scope else scope.encode()).decode() + else: scope = self.scope + except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") if not exclusive_create: - response = requests.patch(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", - headers={"x-api-key": self.__api_key}, params={ - "eTag": str(previous_value), - "allow_missing": not previous_value - }, data=str(value)) + response = requests.patch(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", + headers={"x-api-key": self.__api_key}, params={"allow_missing": True}, json={ + "value": value + }) else: - response = requests.post(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", - headers={"x-api-key": self.__api_key}, data=str(value)) + response = requests.post(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries", + headers={"x-api-key": self.__api_key}, json={ + "path": key, + "value": value + }) - if response.status_code == 400 and response.json()["errorDetails"][0].get("datastoreErrorCode") == "EntryAlreadyExists": + if exclusive_create and response.status_code == 400 and response.json()["code"] == 3: raise PreconditionFailed(None, None, f"An entry already exists with the provided key and scope") if response.status_code == 200: return int(response.json()["value"]) @@ -440,8 +432,6 @@ def set(self, key: str, value: int, exclusive_create: bool=False, previous_value elif response.status_code in [404, 409]: if exclusive_create: error = "An entry already exists with the provided key and scope" - elif previous_value: - error = f"The current value is not {previous_value}" else: error = "A Precondition Failed" @@ -449,22 +439,38 @@ def set(self, key: str, value: int, exclusive_create: bool=False, previous_value if response.status_code == 429: raise RateLimited("You're being rate limited.") elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") - - def remove(self, key: str) -> None: + + def increment(self, key: str, increment: int) -> None: try: if not self.scope: scope, key = key.split("/", maxsplit=1) - except(ValueError): - raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") - - scope = base64.b64encode(self.scope.encode() if self.scope else scope.encode()).decode() - response = requests.delete(f"https://apis.roblox.com/datastores/v2/universes/{self.universe.id}/orderedDatastores/{base64.b64encode(self.name.encode()).decode()}/scopes/{scope}/entries/{base64.b64encode(key.encode()).decode()}", - headers={"x-api-key": self.__api_key}) + else: scope = self.scope + except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") + + response = requests.post(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}:increment", + headers={"x-api-key": self.__api_key}, json={ + "amount": increment + }) - if response.status_code == 204: return None + if response.status_code == 200: return int(response.json()["value"]) elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") elif response.status_code == 404: raise NotFound(f"The key {key} does not exist.") + elif response.status_code == 409 and response.json()["code"] == 10: raise ValueError("New value can't be less than 0.") elif response.status_code == 429: raise RateLimited("You're being rate limited.") elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") -"https://apis.roblox.com/datastores/v2/universes/:universe/orderedDatastores/:datastore/scopes/:scope/entries/:entry" \ No newline at end of file + def remove(self, key: str) -> None: + try: + if not self.scope: scope, key = key.split("/", maxsplit=1) + else: scope = self.scope + except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") + + response = requests.delete(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", + headers={"x-api-key": self.__api_key}) + + if response.status_code == 200: return None + elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") + elif response.status_code == 404: raise NotFound(f"The key {key} does not exist.") + elif response.status_code == 429: raise RateLimited("You're being rate limited.") + elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") + else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") \ No newline at end of file From e5204a6bf567f9f2eaab680f3f4c5f6f23358ab5 Mon Sep 17 00:00:00 2001 From: TreeBen77 <77905642+TreeBen77@users.noreply.github.com> Date: Thu, 9 Mar 2023 19:57:33 +1000 Subject: [PATCH 5/9] added documentation for the new ordered data store! --- docs/source/datastore.rst | 186 +++++++++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 3 deletions(-) diff --git a/docs/source/datastore.rst b/docs/source/datastore.rst index 5ade6ba..f90324f 100644 --- a/docs/source/datastore.rst +++ b/docs/source/datastore.rst @@ -57,7 +57,6 @@ Data Store :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. - .. method:: get(key) @@ -189,6 +188,159 @@ Data Store :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. +.. class:: OrderedDataStore + + Class for interacting with the Ordered DataStore API for a specific Ordered DataStore. + + .. versionadded:: 1.1 + + .. warning:: + + This class is not designed to be created by users. It is returned by :meth:`Universe.get_ordered_data_store`. + + .. attribute:: name + + :type: str + + .. attribute:: scope + + :type: Union[str, None] + + .. attribute:: experince + + :type: rblx-open-cloud.Experince + + .. method:: sort_keys(descending=True, filter=None, limit=None) + + Returns a list of keys and their values. + + The example below would list all keys, along with their value. + + .. code:: py + + for key in datastore.sort_keys(): + print(key.name, key.value) + + You can simply convert it to a list by putting it in the list function: + + .. code:: py + + list(datastore.sort_keys()) + + Lua equivalent: `OrderedDataStore:GetSortedAsync() `__ + + :param bool descending: Wether the largest number should be first, or the smallest. + :param rblx-open-cloud.SortFilter filter: Minimum and maximum requirements for numbers. + :param bool limit: Max number of entries to loop through. + + :returns: Iterable[:class:`SortedEntry`] + :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope and the key must be formatted as ``scope/key`` + :raises rblx-open-cloud.InvalidToken: The token is invalid or doesn't have sufficent permissions to read data store keys. + :raises rblx-open-cloud.NotFound: The datastore or key does not exist + :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. + :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. + :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. + + .. note:: + + Unlike :meth:`DataStore.list_keys`, this function is unable to work without a scope. This is an Open Cloud limitation. You can still use other functions with the normal ``scope/key`` when scope is ``None``. + + .. method:: get(key) + + Gets the value of a key. + + Lua equivalent: `OrderedDataStore:GetAsync() `__ + + :param str key: The key to find. + + :returns: int + :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope and the key must be formatted as ``scope/key`` + :raises rblx-open-cloud.InvalidToken: The token is invalid or doesn't have sufficent permissions to read data store keys. + :raises rblx-open-cloud.NotFound: The datastore or key does not exist + :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. + :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. + :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. + + .. method:: set(key, value, exclusive_create) + + Sets the value of a key. + + Lua equivalent: `OrderedDataStore:SetAsync() `__ + + :param str key: The key to create/update. + :param int value: The new integer value. Must be positive. + :param bool exclusive_create: Wether to fail if the key already has a value. + + :returns: int + :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope and the key must be formatted as ``scope/key`` + :raises rblx-open-cloud.InvalidToken: The token is invalid or doesn't have sufficent permissions to write data store keys. + :raises rblx-open-cloud.NotFound: The datastore or key does not exist + :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. + :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. + :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. + :raises rblx-open-cloud.PreconditionFailed: ``exclusive_create`` is ``True`` and the key already has a value. + + .. method:: increment(key, increment) + + Increments the value of a key. + + Lua equivalent: `OrderedDataStore:IncrementAsync() `__ + + :param str key: The key to increment. + :param int increment: The amount to increment the key by. You can use negative numbers to decrease the value. + + :returns: int + :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope and the key must be formatted as ``scope/key`` + :raises rblx-open-cloud.InvalidToken: The token is invalid or doesn't have sufficent permissions to write data store keys. + :raises rblx-open-cloud.NotFound: The datastore or key does not exist + :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. + :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. + :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. + :raises rblx-open-cloud.PreconditionFailed: ``exclusive_create`` is ``True`` and the key already has a value. + + .. method:: remove(key) + + Removes a key. + + Lua equivalent: `OrderedDataStore:RemoveAsync() `__ + + :param str key: The key to remove. + + :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope and the key must be formatted as ``scope/key`` + :raises rblx-open-cloud.InvalidToken: The token is invalid or doesn't have sufficent permissions to write data store keys. + :raises rblx-open-cloud.NotFound: The datastore or key does not exist + :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. + :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. + :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. + +.. class:: SortFilter + + Filter object for :meth:`OrderedDataStore.sort_keys` + + :param Union[int, None] min: Minimum entry value to retrieve + :param Union[int, None] max: Maximum entry value to retrieve + + .. versionadded:: 1.1 + + .. attribute:: min + + Minimum entry value to retrieve + + :type: Union[int, None] + + .. attribute:: max + + Maximum entry value to retrieve + + :type: Union[int, None] + + .. meth:: to_str + + Generates a string which is usually used internally for the API. + + :returns: Union[str, None] + :raises ValueError: ``min`` is greater than ``max``. + .. class:: EntryInfo Contains data about an entry such as version ID, timestamps, users and metadata. @@ -272,7 +424,7 @@ Data Store .. class:: ListedEntry - Object which contains a entry's key and scope. + Object which contains an entry's key and scope. .. warning:: @@ -288,4 +440,32 @@ Data Store The Entry's scope - :type: str \ No newline at end of file + :type: str + +.. class:: SortedEntry + + Object which contains a sorted entry's key, scope, and value. + + .. versionadded:: 1.1 + + .. warning:: + + This class is not designed to be created by users. It is returned by :meth:`OrderedDataStore.sort_keys`. + + .. attribute:: key + + The Entry's key + + :type: str + + .. attribute:: scope + + The Entry's scope + + :type: str + + .. attribute:: value + + The Entry's value + + :type: int \ No newline at end of file From 4ee38389249ffbad7bc1a208ca2bb4cbf3f3ed39 Mon Sep 17 00:00:00 2001 From: TreeBen77 <77905642+TreeBen77@users.noreply.github.com> Date: Thu, 9 Mar 2023 20:24:58 +1000 Subject: [PATCH 6/9] added exclusive_update to OrderedDataStore.set --- docs/source/datastore.rst | 14 +++++++------- rblxopencloud/datastore.py | 7 +++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/source/datastore.rst b/docs/source/datastore.rst index f90324f..6a17382 100644 --- a/docs/source/datastore.rst +++ b/docs/source/datastore.rst @@ -208,7 +208,7 @@ Data Store .. attribute:: experince - :type: rblx-open-cloud.Experince + :type: :meth:`rblx-open-cloud.Experince` .. method:: sort_keys(descending=True, filter=None, limit=None) @@ -243,7 +243,7 @@ Data Store .. note:: - Unlike :meth:`DataStore.list_keys`, this function is unable to work without a scope. This is an Open Cloud limitation. You can still use other functions with the normal ``scope/key`` when scope is ``None``. + Unlike :meth:`DataStore.list_keys`, this function is unable to work without a scope. This is an Open Cloud limitation. You can still use other functions with the normal ``scope/key`` syntax when scope is ``None``. .. method:: get(key) @@ -261,7 +261,7 @@ Data Store :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. - .. method:: set(key, value, exclusive_create) + .. method:: set(key, value, exclusive_create=False, exclusive_update=False) Sets the value of a key. @@ -270,15 +270,16 @@ Data Store :param str key: The key to create/update. :param int value: The new integer value. Must be positive. :param bool exclusive_create: Wether to fail if the key already has a value. + :param bool exclusive_update: Wether to fail if the key does not have a value. :returns: int - :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope and the key must be formatted as ``scope/key`` + :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope and the key must be formatted as ``scope/key`` or both ``exclusive_create`` and ``exclusive_update`` are ``True``. :raises rblx-open-cloud.InvalidToken: The token is invalid or doesn't have sufficent permissions to write data store keys. :raises rblx-open-cloud.NotFound: The datastore or key does not exist :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. - :raises rblx-open-cloud.PreconditionFailed: ``exclusive_create`` is ``True`` and the key already has a value. + :raises rblx-open-cloud.PreconditionFailed: ``exclusive_create`` is ``True`` and the key already has a value, or ``exclusive_update`` is ``True`` and there is no pre-existing value. .. method:: increment(key, increment) @@ -296,8 +297,7 @@ Data Store :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. - :raises rblx-open-cloud.PreconditionFailed: ``exclusive_create`` is ``True`` and the key already has a value. - + .. method:: remove(key) Removes a key. diff --git a/rblxopencloud/datastore.py b/rblxopencloud/datastore.py index 394f7d5..e3ddfa8 100644 --- a/rblxopencloud/datastore.py +++ b/rblxopencloud/datastore.py @@ -406,15 +406,16 @@ def get(self, key: str) -> int: elif response.status_code >= 500: raise ServiceUnavailable("The service is unavailable or has encountered an error.") else: raise rblx_opencloudException(f"Unexpected HTTP {response.status_code}") - def set(self, key: str, value: int, exclusive_create: bool=False) -> int: + def set(self, key: str, value: int, exclusive_create: bool=False, exclusive_update: bool=False) -> int: try: if not self.scope: scope, key = key.split("/", maxsplit=1) else: scope = self.scope except(ValueError): raise ValueError("a scope and key seperated by a forward slash is required for OrderedDataStore without a scope.") + if exclusive_create and exclusive_update: raise ValueError("exclusive_create and exclusive_updated can not both be True") if not exclusive_create: response = requests.patch(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries/{urllib.parse.quote(key)}", - headers={"x-api-key": self.__api_key}, params={"allow_missing": True}, json={ + headers={"x-api-key": self.__api_key}, params={"allow_missing": not exclusive_update}, json={ "value": value }) else: @@ -432,6 +433,8 @@ def set(self, key: str, value: int, exclusive_create: bool=False) -> int: elif response.status_code in [404, 409]: if exclusive_create: error = "An entry already exists with the provided key and scope" + elif exclusive_update: + error = "There is no pre-existing entry with the provided key" else: error = "A Precondition Failed" From edb2f4cc71d35a51f8907358330993f3186745ab Mon Sep 17 00:00:00 2001 From: TreeBen77 <77905642+TreeBen77@users.noreply.github.com> Date: Fri, 17 Mar 2023 08:45:23 +1000 Subject: [PATCH 7/9] removed filters and replaced with min and max --- docs/source/datastore.rst | 33 +++--------------------------- rblxopencloud/datastore.py | 42 +++++++++++--------------------------- 2 files changed, 15 insertions(+), 60 deletions(-) diff --git a/docs/source/datastore.rst b/docs/source/datastore.rst index 6a17382..dbb593b 100644 --- a/docs/source/datastore.rst +++ b/docs/source/datastore.rst @@ -210,7 +210,7 @@ Data Store :type: :meth:`rblx-open-cloud.Experince` - .. method:: sort_keys(descending=True, filter=None, limit=None) + .. method:: sort_keys(descending=True, limit=None, min=None, max=None) Returns a list of keys and their values. @@ -230,8 +230,9 @@ Data Store Lua equivalent: `OrderedDataStore:GetSortedAsync() `__ :param bool descending: Wether the largest number should be first, or the smallest. - :param rblx-open-cloud.SortFilter filter: Minimum and maximum requirements for numbers. :param bool limit: Max number of entries to loop through. + :param int min: Minimum entry value to retrieve + :param int max: Maximum entry value to retrieve. :returns: Iterable[:class:`SortedEntry`] :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope and the key must be formatted as ``scope/key`` @@ -313,34 +314,6 @@ Data Store :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. -.. class:: SortFilter - - Filter object for :meth:`OrderedDataStore.sort_keys` - - :param Union[int, None] min: Minimum entry value to retrieve - :param Union[int, None] max: Maximum entry value to retrieve - - .. versionadded:: 1.1 - - .. attribute:: min - - Minimum entry value to retrieve - - :type: Union[int, None] - - .. attribute:: max - - Maximum entry value to retrieve - - :type: Union[int, None] - - .. meth:: to_str - - Generates a string which is usually used internally for the API. - - :returns: Union[str, None] - :raises ValueError: ``min`` is greater than ``max``. - .. class:: EntryInfo Contains data about an entry such as version ID, timestamps, users and metadata. diff --git a/rblxopencloud/datastore.py b/rblxopencloud/datastore.py index e3ddfa8..d4ddd02 100644 --- a/rblxopencloud/datastore.py +++ b/rblxopencloud/datastore.py @@ -324,33 +324,6 @@ def __eq__(self, object) -> bool: def __repr__(self) -> str: return f"rblxopencloud.SortedEntry(\"{self.key}\", value={self.value})" -class SortFilter(): - def __init__(self, min: Union[int, None], max: Union[int, None]) -> None: - self.min: Union[int, None] = min - self.max: Union[int, None] = max - - def __eq__(self, object) -> bool: - if not isinstance(object, SortFilter): - return NotImplemented - return self.min == object.min and self.max == object.max - - def __repr__(self) -> str: - return f"rblxopencloud.SortFilter(min=\"{self.min}\", max=\"{self.max}\")"\ - - def to_str(self) -> Union[str, None]: - if self.min and self.max: - if self.min > self.max: - raise ValueError("minimum must not be greater than max.") - return f"entry >= {self.min} && entry <= {self.max}" - - if self.min: - return f"entry >= {self.min}" - - if self.min: - return f"entry <= {self.max}" - - return None - class OrderedDataStore(): def __init__(self, name, experince, api_key, scope): self.name = name @@ -364,8 +337,17 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.name - def sort_keys(self, descending: bool=True, filter: Union[SortFilter, None]=None, limit: Union[None, int]=None) -> Iterable[SortedEntry]: + def sort_keys(self, descending: bool=True, limit: Union[None, int]=None, min=None, max=None) -> Iterable[SortedEntry]: if not self.scope: raise ValueError("A scope is required to list keys on OrderedDataStore.") + + filter = None + if min and max: + if min > max: raise ValueError("min must not be greater than max.") + filter = f"entry >= {min} && entry <= {max}" + + if min: filter = f"entry >= {min}" + if max: filter = f"entry <= {max}" + nextcursor = "" yields = 0 while limit == None or yields < limit: @@ -374,7 +356,7 @@ def sort_keys(self, descending: bool=True, filter: Union[SortFilter, None]=None, "max_page_size": limit if limit and limit < 100 else 100, "order_by": "desc" if descending else None, "page_token": nextcursor if nextcursor else None, - "filter": filter.to_str() if filter else None + "filter": filter }) if response.status_code == 401 or response.status_code == 403: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") elif response.status_code == 404: raise NotFound("The datastore you're trying to access does not exist.") @@ -387,7 +369,7 @@ def sort_keys(self, descending: bool=True, filter: Union[SortFilter, None]=None, yields += 1 yield SortedEntry(key["path"], key["value"], self.scope) if limit != None and yields >= limit: break - nextcursor = data.get("lastEvaluatedKey") + nextcursor = data.get("nextPageToken") if not nextcursor: break def get(self, key: str) -> int: From 2048d254e9a6f46c6620fdeff66b6259598fc122 Mon Sep 17 00:00:00 2001 From: TreeBen77 <77905642+TreeBen77@users.noreply.github.com> Date: Fri, 31 Mar 2023 11:50:59 +1000 Subject: [PATCH 8/9] updated to new ordered data store api --- rblxopencloud/datastore.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/rblxopencloud/datastore.py b/rblxopencloud/datastore.py index d4ddd02..9925617 100644 --- a/rblxopencloud/datastore.py +++ b/rblxopencloud/datastore.py @@ -9,7 +9,6 @@ "ListedEntry", "DataStore", "SortedEntry", - "SortFilter", "OrderedDataStore" ) @@ -367,7 +366,7 @@ def sort_keys(self, descending: bool=True, limit: Union[None, int]=None, min=Non data = response.json() for key in data["entries"]: yields += 1 - yield SortedEntry(key["path"], key["value"], self.scope) + yield SortedEntry(key["id"], key["value"], self.scope) if limit != None and yields >= limit: break nextcursor = data.get("nextPageToken") if not nextcursor: break @@ -402,15 +401,13 @@ def set(self, key: str, value: int, exclusive_create: bool=False, exclusive_upda }) else: response = requests.post(f"https://apis.roblox.com/ordered-data-stores/v1/universes/{self.experince.id}/orderedDatastores/{urllib.parse.quote(self.name)}/scopes/{urllib.parse.quote(scope)}/entries", - headers={"x-api-key": self.__api_key}, json={ - "path": key, + headers={"x-api-key": self.__api_key}, params={"id": key}, json={ "value": value }) - if exclusive_create and response.status_code == 400 and response.json()["code"] == 3: + if exclusive_create and response.status_code == 400 and response.json()["code"] == "INVALID_ARGUMENT": raise PreconditionFailed(None, None, f"An entry already exists with the provided key and scope") - - if response.status_code == 200: return int(response.json()["value"]) + elif response.status_code == 200: return int(response.json()["value"]) elif response.status_code == 401: raise InvalidKey("Your key may have expired, or may not have permission to access this resource.") elif response.status_code in [404, 409]: if exclusive_create: From a2e822989c43ec4928b7bca03968ef10b433d3f3 Mon Sep 17 00:00:00 2001 From: TreeBen77 <77905642+TreeBen77@users.noreply.github.com> Date: Fri, 31 Mar 2023 15:20:48 +1000 Subject: [PATCH 9/9] updated docs --- docs/source/datastore.rst | 14 +++++++------- docs/source/universe.rst | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/source/datastore.rst b/docs/source/datastore.rst index dbb593b..9e0c357 100644 --- a/docs/source/datastore.rst +++ b/docs/source/datastore.rst @@ -192,11 +192,11 @@ Data Store Class for interacting with the Ordered DataStore API for a specific Ordered DataStore. - .. versionadded:: 1.1 + .. versionadded:: 1.2 .. warning:: - This class is not designed to be created by users. It is returned by :meth:`Universe.get_ordered_data_store`. + This class is not designed to be created by users. It is returned by :meth:`Experince.get_ordered_data_store`. .. attribute:: name @@ -204,7 +204,7 @@ Data Store .. attribute:: scope - :type: Union[str, None] + :type: Optional[str] .. attribute:: experince @@ -235,8 +235,8 @@ Data Store :param int max: Maximum entry value to retrieve. :returns: Iterable[:class:`SortedEntry`] - :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope and the key must be formatted as ``scope/key`` - :raises rblx-open-cloud.InvalidToken: The token is invalid or doesn't have sufficent permissions to read data store keys. + :raises ValueError: The :class:`OrderedDataStore` doesn't have a scope. + :raises rblx-open-cloud.InvalidToken: The token is invalid or doesn't have sufficent permissions to list data store keys. :raises rblx-open-cloud.NotFound: The datastore or key does not exist :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. @@ -244,7 +244,7 @@ Data Store .. note:: - Unlike :meth:`DataStore.list_keys`, this function is unable to work without a scope. This is an Open Cloud limitation. You can still use other functions with the normal ``scope/key`` syntax when scope is ``None``. + Unlike :meth:`DataStore.list_keys`, this function is unable to work without a scope, this is an Open Cloud limitation. You can still use other functions with the normal ``scope/key`` syntax when scope is ``None``. .. method:: get(key) @@ -419,7 +419,7 @@ Data Store Object which contains a sorted entry's key, scope, and value. - .. versionadded:: 1.1 + .. versionadded:: 1.2 .. warning:: diff --git a/docs/source/universe.rst b/docs/source/universe.rst index cafd3c7..66f1cc1 100644 --- a/docs/source/universe.rst +++ b/docs/source/universe.rst @@ -27,9 +27,6 @@ Universe :param str name: The name of the data store :param Union[str, None] scope: A string specifying the scope, can also be None. :returns: :class:`rblx-open-cloud.DataStore` - - .. note:: - Roblox does not support accessing OrderedDataStores with Open Cloud. .. method:: list_data_stores(prefix="", scope="global") @@ -60,6 +57,18 @@ Universe :raises rblx-open-cloud.RateLimited: You're being rate limited by Roblox. Try again in a minute. :raises rblx-open-cloud.ServiceUnavailable: Roblox's services as currently experiencing downtime. :raises rblx-open-cloud.rblx_opencloudException: Roblox's response was unexpected. + + .. method:: get_ordered_data_store(name, scope="global") + + Creates a :class:`rblx-open-cloud.OrderedDataStore` with the provided name and scope. + + If ``scope`` is ``None`` then keys require to be formatted like ``scope/key`` and :meth:`OrderedDataStore.sort_keys` will not work. + + Lua equivalent: `DataStoreService:GetDataStore() `__ + + :param str name: The name of the data store + :param Union[str, None] scope: A string specifying the scope, can also be None. + :returns: :class:`rblx-open-cloud.DataStore` .. method:: publish_message(topic, data)