Skip to content

Commit 57d09a9

Browse files
authored
Releases v0.12.5 (#302)
1 parent f2a5808 commit 57d09a9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1039
-168
lines changed

README.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ Or from source code (not recommended for production use):
3232
Dependencies
3333
------------
3434

35-
- Python (>=2.7), including Python 3+, pypy, Python 3.7 recommended
36-
- setuptools (>=3.0)
35+
- Python (>=2.7), including Python 3+, pypy, Python 3.7 recommended
36+
- setuptools (>=3.0)
3737

3838
Run Tests
3939
---------
4040

41-
- install pytest
42-
- copy conf/test.conf.template to odps/tests/test.conf, and fill it
43-
with your account
44-
- run ``pytest odps``
41+
- install pytest
42+
- copy conf/test.conf.template to odps/tests/test.conf, and fill it with
43+
your account
44+
- run ``pytest odps``
4545

4646
Usage
4747
-----

odps/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
version_info = (0, 12, 5, "b1")
15+
version_info = (0, 12, 5)
1616
_num_index = max(idx if isinstance(v, int) else 0 for idx, v in enumerate(version_info))
1717
__version__ = ".".join(map(str, version_info[: _num_index + 1])) + "".join(
1818
version_info[_num_index + 1 :]

odps/apis/storage_api/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 1999-2024 Alibaba Group Holding Ltd.
1+
# Copyright 1999-2025 Alibaba Group Holding Ltd.
22
#
33
# Licensed under the Apache License, Version 2.0 (the "License");
44
# you may not use this file except in compliance with the License.
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
1516
import sys
1617

1718
import pytest
@@ -42,8 +43,9 @@ def storage_api_client(odps):
4243
("a BIGINT, b BIGINT, c BIGINT, d BIGINT", "pt string"),
4344
if_not_exists=True,
4445
)
46+
api_endpoint = os.getenv("ODPS_STORAGE_API_ENDPOINT")
4547
try:
46-
yield StorageApiArrowClient(odps, table)
48+
yield StorageApiArrowClient(odps, table, rest_endpoint=api_endpoint)
4749
finally:
4850
table.drop(async_=True)
4951
options.enable_schema = False

odps/apis/storage_api/storage_api.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def __init__(self, **kwargs):
197197

198198

199199
class TableBatchScanResponse(serializers.JSONSerializableModel):
200-
__slots__ = ["status", "request_id"]
200+
__slots__ = ("status", "request_id")
201201

202202
session_id = serializers.JSONNodeField("SessionId")
203203
session_type = serializers.JSONNodeField("SessionType")
@@ -655,8 +655,9 @@ def tunnel_rest(self):
655655
self._tunnel_rest = tunnel.tunnel_rest
656656
return self._tunnel_rest
657657

658-
def _get_resource(self, *args) -> str:
659-
endpoint = self.tunnel_rest.endpoint + URL_PREFIX
658+
def _get_resource(self, *args, url_prefix=None) -> str:
659+
url_prefix = url_prefix or URL_PREFIX
660+
endpoint = self.tunnel_rest.endpoint + url_prefix
660661
url = self._table.table_resource(endpoint=endpoint, force_schema=True)
661662
return "/".join([url] + list(args))
662663

@@ -917,6 +918,16 @@ def commit_write_session(
917918
return response
918919

919920

921+
try:
922+
from ...internal.apis.storage_api.storage_api import InternalStorageAPIClientMixin
923+
924+
StorageApiClient = type(
925+
"StorageApiClient", (InternalStorageAPIClientMixin, StorageApiClient), {}
926+
)
927+
except ImportError:
928+
pass
929+
930+
920931
class StorageApiArrowClient(StorageApiClient):
921932
"""Arrow batch client to bundle configuration needed for API requests."""
922933

@@ -951,3 +962,14 @@ def write_rows_arrow(self, request: WriteRowsRequest) -> ArrowWriter:
951962
)
952963

953964
return ArrowWriter(self.write_rows_stream(request), request.compression)
965+
966+
967+
try:
968+
from ...internal.apis.storage_api.storage_api import ( # noqa: F401
969+
CloseBlobWriterRequest,
970+
CreateBlobWriterRequest,
971+
ReadBlobRequest,
972+
WriteBlobRequest,
973+
)
974+
except ImportError:
975+
pass

odps/config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ def emit(self, record):
502502
default_options.register_option("enable_v4_sign", True, validator=is_bool)
503503
default_options.register_option("align_supported_python_tag", True, validator=is_bool)
504504

505-
# c or python mode, use for UT, in other cases, please do not modify the value
505+
# c or python mode for UT only. In other cases, please do not modify the value
506506
default_options.register_option("force_c", False, validator=is_integer)
507507
default_options.register_option("force_py", False, validator=is_integer)
508508

@@ -612,6 +612,14 @@ def emit(self, record):
612612
"sql.use_odps2_extension", None, validator=any_validator(is_null, is_bool)
613613
)
614614

615+
# Catalog API
616+
default_options.register_option(
617+
"catalog.endpoint", None, validator=any_validator(is_null, is_string)
618+
)
619+
default_options.register_option(
620+
"catalog.url_prefix", "/api/catalog/v1alpha", validator=is_string
621+
)
622+
615623
# sqlalchemy
616624
default_options.register_option(
617625
"sqlalchemy.project_as_schema", None, validator=any_validator(is_null, is_bool)

odps/core.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
DEFAULT_ENDPOINT = "http://service.odps.aliyun.com/api"
3030
DEFAULT_REGION_NAME = "cn"
3131
LOGVIEW_HOST_DEFAULT = "http://logview.aliyun.com"
32+
CATALOG_API_ENDPOINT_DEFAULT = "https://catalogapi.{region}.maxcompute.aliyun.com"
3233

3334
_ALTER_TABLE_REGEX = re.compile(
3435
r"^\s*(drop|alter)\s+table\s*(|if\s+exists)\s+(?P<table_name>[^\s;]+)", re.I
@@ -104,6 +105,7 @@ def __init__(
104105
region_name=None,
105106
quota_name=None,
106107
namespace=None,
108+
catalog_endpoint=None,
107109
**kw
108110
):
109111
# avoid polluted copy sources :(
@@ -114,6 +116,7 @@ def __init__(
114116
schema = utils.strip_if_str(schema)
115117
logview_host = utils.strip_if_str(logview_host)
116118
tunnel_endpoint = utils.strip_if_str(tunnel_endpoint)
119+
catalog_endpoint = utils.strip_if_str(catalog_endpoint)
117120
region_name = utils.strip_if_str(region_name)
118121
quota_name = utils.strip_if_str(quota_name)
119122
namespace = utils.strip_if_str(namespace)
@@ -133,6 +136,7 @@ def __init__(
133136
app_account=app_account,
134137
logview_host=logview_host,
135138
tunnel_endpoint=tunnel_endpoint,
139+
catalog_endpoint=catalog_endpoint,
136140
region_name=region_name,
137141
quota_name=quota_name,
138142
namespace=namespace,
@@ -178,7 +182,11 @@ def _init(
178182
self.project = (
179183
project or options.default_project or os.getenv("ODPS_PROJECT_NAME")
180184
)
181-
self.region_name = region_name or self._get_region_from_endpoint(self.endpoint)
185+
self.region_name = (
186+
region_name
187+
or os.getenv("ODPS_REGION_NAME")
188+
or self._get_region_from_endpoint(self.endpoint)
189+
)
182190
self.namespace = (
183191
namespace or options.default_namespace or os.getenv("ODPS_NAMESPACE")
184192
)
@@ -206,6 +214,32 @@ def _init(
206214
or os.getenv("ODPS_TUNNEL_ENDPOINT")
207215
)
208216

217+
self._catalog_endpoint = (
218+
kw.pop("catalog_endpoint", None)
219+
or options.catalog.endpoint
220+
or os.getenv("ODPS_CATALOG_ENDPOINT")
221+
or (
222+
CATALOG_API_ENDPOINT_DEFAULT.format(region=self.region_name)
223+
if self.region_name and CATALOG_API_ENDPOINT_DEFAULT
224+
else None
225+
)
226+
)
227+
if self._catalog_endpoint is None:
228+
self.catalog_rest = None
229+
else:
230+
self.catalog_rest = rest_client_cls(
231+
self.account,
232+
self._catalog_endpoint.rstrip("/"),
233+
project,
234+
schema,
235+
app_account=self.app_account,
236+
proxy=options.api_proxy,
237+
region_name=self.region_name,
238+
namespace=self.namespace,
239+
tag="Catalog",
240+
**rest_client_kwargs
241+
)
242+
209243
self._logview_host = (
210244
kw.pop("logview_host", None)
211245
or options.logview_host
@@ -506,6 +540,7 @@ def get_project(self, name=None, default_schema=None):
506540
# use _schema to avoid requesting for tenant options
507541
proj._default_schema = default_schema or self._schema
508542
proj._quota_name = self._quota_name
543+
proj._catalog_endpoint = self._catalog_endpoint
509544

510545
proj_ref = weakref.ref(proj)
511546

@@ -2137,6 +2172,45 @@ def delete_offline_model(self, name, project=None, if_exists=False):
21372172
if not if_exists:
21382173
raise
21392174

2175+
def list_models(self, project=None, schema=None):
2176+
"""
2177+
List models of project by optional filter conditions including prefix and owner.
2178+
2179+
:param project: project name, if not provided, will be the default project
2180+
:param str schema: schema name, if not provided, will be the default schema
2181+
:return: models
2182+
:rtype: list
2183+
"""
2184+
parent = self._get_project_or_schema(project, schema)
2185+
return parent.models.iterate()
2186+
2187+
def get_model(self, name, project=None, schema=None):
2188+
"""
2189+
Get model by given name
2190+
2191+
:param name: model name
2192+
:param project: project name, if not provided, will be the default project
2193+
:param str schema: schema name, if not provided, will be the default schema
2194+
:return: model
2195+
:rtype: :class:`odps.models.ml.Model`
2196+
:raise: :class:`odps.errors.NoSuchObject` if not exists
2197+
"""
2198+
parent = self._get_project_or_schema(project, schema)
2199+
return parent.models[name]
2200+
2201+
def exist_model(self, name, project=None, schema=None):
2202+
"""
2203+
If the model with given name exists or not.
2204+
2205+
:param name: model's name
2206+
:param project: project name, if not provided, will be the default project
2207+
:param str schema: schema name, if not provided, will be the default schema
2208+
:return: True if model exists else False
2209+
:rtype: bool
2210+
"""
2211+
parent = self._get_project_or_schema(project, schema)
2212+
return name in parent.models
2213+
21402214
def get_logview_host(self):
21412215
"""
21422216
Get logview host address.
@@ -2514,7 +2588,8 @@ def _get_odps_from_model(self):
25142588
return cur.odps if cur else None
25152589

25162590

2517-
models.RestModel.odps = property(fget=_get_odps_from_model)
2591+
models.JSONRestModel.odps = property(fget=_get_odps_from_model)
2592+
models.XMLRestModel.odps = property(fget=_get_odps_from_model)
25182593
del _get_odps_from_model
25192594

25202595
try:

odps/errors.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,17 @@ def parse_response(resp, endpoint=None, tag=None):
5555
request_id = resp.headers.get("x-odps-request-id", None)
5656
if len(resp.content) > 0:
5757
obj = json.loads(resp.text)
58-
msg = obj["Message"]
59-
code = obj.get("Code")
60-
host_id = obj.get("HostId")
61-
if request_id is None:
62-
request_id = obj.get("RequestId")
58+
if tag == "Catalog":
59+
msg = obj["message"]
60+
reason = obj.get("reason")
61+
code = _CATALOG_ERROR_MAPPING.get(reason, reason)
62+
host_id = None
63+
else:
64+
msg = obj["Message"]
65+
code = obj.get("Code")
66+
host_id = obj.get("HostId")
67+
if request_id is None:
68+
request_id = obj.get("RequestId")
6369
else:
6470
raise
6571
clz = globals().get(code, ODPSError)
@@ -126,6 +132,10 @@ def throw_if_parsable(resp, endpoint=None, tag=None):
126132
"ODPS-186": "SQAQueryTimedout",
127133
}
128134

135+
_CATALOG_ERROR_MAPPING = {
136+
"NotFound": "NoSuchObject",
137+
}
138+
129139
_nginx_bad_gateway_message = "the page you are looking for is currently unavailable"
130140

131141

odps/models/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3-
# Copyright 1999-2024 Alibaba Group Holding Ltd.
3+
# Copyright 1999-2025 Alibaba Group Holding Ltd.
44
#
55
# Licensed under the Apache License, Version 2.0 (the "License");
66
# you may not use this file except in compliance with the License.
@@ -17,7 +17,7 @@
1717
import sys
1818
import warnings
1919

20-
from .core import RestModel
20+
from .core import JSONRestModel, XMLRestModel
2121
from .function import Function
2222
from .functions import Functions
2323
from .instance import Instance

odps/models/cache.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def cache_container(self, func, cls, **kwargs):
101101
return obj
102102

103103
def del_item_cache(self, obj, item):
104-
from .core import LazyLoad
104+
from .core import XMLLazyLoad
105105

106106
try:
107107
item = obj._get_parent_typed(item)
@@ -118,38 +118,42 @@ def del_item_cache(self, obj, item):
118118
if cache_key in self._caches:
119119
# make sure original object will be reloaded
120120
obj = self._caches[cache_key]
121-
if isinstance(obj, LazyLoad):
121+
if isinstance(obj, XMLLazyLoad):
122122
obj.reset()
123123

124124
del self._caches[cache_key]
125125

126126

127127
_object_cache = ObjectCache()
128+
_cls_bases_cache = dict()
128129

129130

130131
def cache(func):
132+
@six.wraps(func)
131133
def inner(cls, **kwargs):
132-
bases = [base.__name__ for base in inspect.getmro(cls)]
133-
if "LazyLoad" in bases:
134+
try:
135+
bases_set = _cls_bases_cache[cls]
136+
except KeyError:
137+
bases_set = set(base.__name__ for base in inspect.getmro(cls))
138+
_cls_bases_cache[cls] = bases_set
139+
140+
if "XMLLazyLoad" in bases_set:
134141
return _object_cache.cache_lazyload(func, cls, **kwargs)
135-
elif "Container" in bases:
142+
elif "XMLContainer" in bases_set:
136143
return _object_cache.cache_container(func, cls, **kwargs)
137144

138145
return func(cls, **kwargs)
139146

140-
inner.__name__ = func.__name__
141-
inner.__doc__ = func.__doc__
142147
return inner
143148

144149

145150
def del_cache(func):
151+
@six.wraps(func)
146152
def inner(obj, item):
147153
if func.__name__ == "__delitem__":
148154
_object_cache.del_item_cache(obj, item)
149155
return func(obj, item)
150156

151-
inner.__name__ = func.__name__
152-
inner.__doc__ = func.__doc__
153157
inner._cache_maker = True
154158
return inner
155159

0 commit comments

Comments
 (0)