Skip to content

Commit 48d6b5a

Browse files
committed
Update changelog + test
Signed-off-by: Paul Van Eck <[email protected]>
1 parent 544dc6f commit 48d6b5a

File tree

3 files changed

+263
-5
lines changed

3 files changed

+263
-5
lines changed

sdk/tables/azure-data-tables/CHANGELOG.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
# Release History
22

3-
## 12.7.0 (Unreleased)
3+
## 12.7.0 (2025-05-06)
44

55
### Features Added
66

77
* Added support for configuring custom audiences for `TokenCredential` authentication when initializing a `TableClient` or `TableServiceClient`. ([#40487](https://github.com/Azure/azure-sdk-for-python/pull/40487))
88

9-
### Breaking Changes
10-
11-
### Bugs Fixed
12-
139
### Other Changes
1410

11+
* Python 3.8 is no longer supported. Please use Python version 3.9 or later.
12+
1513
## 12.6.0 (2024-11-21)
1614

1715
### Features Added
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
from typing import Union, Dict, Any, Optional
7+
from urllib.parse import quote
8+
from uuid import UUID
9+
from datetime import datetime, timezone
10+
11+
from ._entity import EntityProperty, EdmType, TableEntity
12+
from ._common_conversion import _decode_base64_to_bytes
13+
14+
15+
class TablesEntityDatetime(datetime):
16+
@property
17+
def tables_service_value(self):
18+
try:
19+
return self._service_value
20+
except AttributeError:
21+
return ""
22+
23+
24+
def url_quote(url):
25+
return quote(url)
26+
27+
28+
def get_enum_value(value):
29+
if value is None or value in ["None", ""]:
30+
return None
31+
try:
32+
return value.value
33+
except AttributeError:
34+
return value
35+
36+
37+
def _from_entity_binary(value: str) -> bytes:
38+
return _decode_base64_to_bytes(value)
39+
40+
41+
def _from_entity_int32(value: str) -> int:
42+
return int(value)
43+
44+
45+
def _from_entity_int64(value: str) -> EntityProperty:
46+
return EntityProperty(int(value), EdmType.INT64)
47+
48+
49+
def _from_entity_datetime(value):
50+
# Cosmos returns this with a decimal point that throws an error on deserialization
51+
cleaned_value = clean_up_dotnet_timestamps(value)
52+
try:
53+
dt_obj = TablesEntityDatetime.strptime(cleaned_value, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
54+
except ValueError:
55+
dt_obj = TablesEntityDatetime.strptime(cleaned_value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
56+
dt_obj._service_value = value # pylint:disable=protected-access,assigning-non-slot
57+
return dt_obj
58+
59+
60+
def clean_up_dotnet_timestamps(value):
61+
# .NET has more decimal places than Python supports in datetime objects, this truncates
62+
# values after 6 decimal places.
63+
value = value.split(".")
64+
ms = ""
65+
if len(value) == 2:
66+
ms = value[-1].replace("Z", "")
67+
if len(ms) > 6:
68+
ms = ms[:6]
69+
ms = ms + "Z"
70+
return ".".join([value[0], ms])
71+
72+
return value[0]
73+
74+
75+
def deserialize_iso(value):
76+
if not value:
77+
return value
78+
return _from_entity_datetime(value)
79+
80+
81+
def _from_entity_guid(value):
82+
return UUID(value)
83+
84+
85+
def _from_entity_str(value: Union[str, bytes]) -> str:
86+
if isinstance(value, bytes):
87+
return value.decode("utf-8")
88+
return value
89+
90+
91+
_EDM_TYPES = [
92+
EdmType.BINARY,
93+
EdmType.INT64,
94+
EdmType.GUID,
95+
EdmType.DATETIME,
96+
EdmType.STRING,
97+
EdmType.INT32,
98+
EdmType.DOUBLE,
99+
EdmType.BOOLEAN,
100+
]
101+
102+
_ENTITY_TO_PYTHON_CONVERSIONS = {
103+
EdmType.BINARY: _from_entity_binary,
104+
EdmType.INT32: _from_entity_int32,
105+
EdmType.INT64: _from_entity_int64,
106+
EdmType.DOUBLE: float,
107+
EdmType.DATETIME: _from_entity_datetime,
108+
EdmType.GUID: _from_entity_guid,
109+
EdmType.STRING: _from_entity_str,
110+
}
111+
112+
113+
def _convert_to_entity(entry_element):
114+
"""Convert json response to entity.
115+
The entity format is:
116+
{
117+
"Address":"Mountain View",
118+
"Age":23,
119+
"AmountDue":200.23,
120+
"[email protected]":"Edm.Guid",
121+
"CustomerCode":"c9da6455-213d-42c9-9a79-3e9149a57833",
122+
"[email protected]":"Edm.DateTime",
123+
"CustomerSince":"2008-07-10T00:00:00",
124+
"IsActive":true,
125+
"[email protected]":"Edm.Int64",
126+
"NumberOfOrders":"255",
127+
"PartitionKey":"my_partition_key",
128+
"RowKey":"my_row_key"
129+
}
130+
131+
:param entry_element: The entity in response.
132+
:type entry_element: Mapping[str, Any]
133+
:return: An entity dict with additional metadata.
134+
:rtype: dict[str, Any]
135+
"""
136+
entity = TableEntity()
137+
138+
properties = {}
139+
edmtypes = {}
140+
odata = {}
141+
142+
for name, value in entry_element.items():
143+
if name.startswith("odata."):
144+
odata[name[6:]] = value
145+
elif name.endswith("@odata.type"):
146+
edmtypes[name[:-11]] = value
147+
else:
148+
properties[name] = value
149+
150+
# TODO: investigate whether we can entirely remove lines 160-168
151+
# Partition key is a known property
152+
partition_key = properties.pop("PartitionKey", None)
153+
if partition_key is not None:
154+
entity["PartitionKey"] = partition_key
155+
156+
# Row key is a known property
157+
row_key = properties.pop("RowKey", None)
158+
if row_key is not None:
159+
entity["RowKey"] = row_key
160+
161+
# Timestamp is a known property
162+
timestamp = properties.pop("Timestamp", None)
163+
164+
for name, value in properties.items():
165+
mtype = edmtypes.get(name)
166+
167+
# Add type for Int32/64
168+
if isinstance(value, int) and mtype is None:
169+
mtype = EdmType.INT32
170+
171+
if value >= 2**31 or value < (-(2**31)):
172+
mtype = EdmType.INT64
173+
174+
# Add type for String
175+
if isinstance(value, str) and mtype is None:
176+
mtype = EdmType.STRING
177+
178+
# no type info, property should parse automatically
179+
if not mtype:
180+
entity[name] = value
181+
elif mtype in [EdmType.STRING, EdmType.INT32]:
182+
entity[name] = value
183+
else: # need an object to hold the property
184+
convert = _ENTITY_TO_PYTHON_CONVERSIONS.get(mtype)
185+
if convert is not None:
186+
new_property = convert(value)
187+
else:
188+
new_property = EntityProperty(mtype, value)
189+
entity[name] = new_property
190+
191+
# extract etag from entry
192+
etag = odata.pop("etag", None)
193+
odata.pop("metadata", None)
194+
if timestamp:
195+
if not etag:
196+
etag = "W/\"datetime'" + url_quote(timestamp) + "'\""
197+
timestamp = _from_entity_datetime(timestamp)
198+
odata.update({"etag": etag, "timestamp": timestamp})
199+
entity._metadata = odata # pylint: disable=protected-access
200+
return entity
201+
202+
203+
def _extract_etag(response):
204+
"""Extracts the etag from the response headers.
205+
206+
:param response: The PipelineResponse object.
207+
:type response: ~azure.core.pipeline.PipelineResponse
208+
:return: The etag from the response headers
209+
:rtype: str or None
210+
"""
211+
if response and response.headers:
212+
return response.headers.get("etag")
213+
214+
return None
215+
216+
217+
def _extract_continuation_token(continuation_token):
218+
"""Extract list entity continuation headers from token.
219+
220+
:param Dict(str,str) continuation_token: The listing continuation token.
221+
:returns: The next partition key and next row key in a tuple
222+
:rtype: (str,str)
223+
"""
224+
if not continuation_token:
225+
return None, None
226+
try:
227+
return continuation_token.get("PartitionKey"), continuation_token.get("RowKey")
228+
except AttributeError as exc:
229+
raise ValueError("Invalid continuation token format.") from exc
230+
231+
232+
def _normalize_headers(headers):
233+
normalized = {}
234+
for key, value in headers.items():
235+
if key.startswith("x-ms-"):
236+
key = key[5:]
237+
normalized[key.lower().replace("-", "_")] = get_enum_value(value)
238+
return normalized
239+
240+
241+
def _return_headers_and_deserialized(_, deserialized, response_headers):
242+
return _normalize_headers(response_headers), deserialized
243+
244+
245+
def _return_context_and_deserialized(response, deserialized, response_headers):
246+
return response.context["location_mode"], deserialized, response_headers
247+
248+
249+
def _trim_service_metadata(metadata: Dict[str, str], content: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
250+
result: Dict[str, Any] = {
251+
"date": metadata.pop("date", None),
252+
"etag": metadata.pop("etag", None),
253+
"version": metadata.pop("version", None),
254+
}
255+
preference = metadata.pop("preference_applied", None)
256+
if preference:
257+
result["preference_applied"] = preference
258+
result["content"] = content
259+
return result

sdk/tables/azure-data-tables/tests/test_table_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def test_client_with_url_ends_with_table_name(
190190
assert ("Please check your account URL.") in str(exc.value)
191191
valid_tc.delete_table()
192192

193+
@pytest.mark.skip("Test missing recording")
193194
@tables_decorator
194195
@recorded_by_proxy
195196
def test_error_handling(self, tables_storage_account_name, tables_primary_storage_account_key):

0 commit comments

Comments
 (0)