Skip to content

Commit 8f5525b

Browse files
Merge branch 'apache:main' into fix-2938-snapshot-timestamp
2 parents 62d4a1f + 4034455 commit 8f5525b

File tree

12 files changed

+133
-8
lines changed

12 files changed

+133
-8
lines changed

.github/workflows/codeql.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
#
19+
20+
name: "CodeQL"
21+
22+
on:
23+
push:
24+
branches: [ "main" ]
25+
pull_request:
26+
branches: [ "main" ]
27+
schedule:
28+
- cron: '16 4 * * 1'
29+
30+
jobs:
31+
analyze:
32+
name: Analyze Actions
33+
runs-on: ubuntu-latest
34+
permissions:
35+
security-events: write
36+
packages: read
37+
38+
steps:
39+
- name: Checkout repository
40+
uses: actions/checkout@v4
41+
42+
- name: Initialize CodeQL
43+
uses: github/codeql-action/init@v4
44+
with:
45+
languages: actions
46+
47+
- name: Perform CodeQL Analysis
48+
uses: github/codeql-action/analyze@v4
49+
with:
50+
category: "/language:actions"

mkdocs/docs/cli.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ hide:
2626

2727
Pyiceberg comes with a CLI that's available after installing the `pyiceberg` package.
2828

29-
You can pass the path to the Catalog using the `--uri` and `--credential` argument, but it is recommended to setup a `~/.pyiceberg.yaml` config as described in the [Catalog](configuration.md) section.
29+
You can pass the path to the Catalog using `--uri` and `--credential`. For REST catalogs that require a path prefix, you can also set `--prefix`. It is still recommended to set up a `~/.pyiceberg.yaml` config as described in the [Catalog](configuration.md) section.
3030

3131
```sh
3232
➜ pyiceberg --help
@@ -39,6 +39,7 @@ Options:
3939
--ugi TEXT
4040
--uri TEXT
4141
--credential TEXT
42+
--prefix TEXT
4243
--help Show this message and exit.
4344

4445
Commands:

pyiceberg/catalog/rest/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ class HttpMethod(str, Enum):
8888
HEAD = "HEAD"
8989
POST = "POST"
9090
DELETE = "DELETE"
91+
PUT = "PUT"
92+
CONNECT = "CONNECT"
93+
OPTIONS = "OPTIONS"
94+
TRACE = "TRACE"
95+
PATCH = "PATCH"
9196

9297

9398
class Endpoint(IcebergBaseModel):
@@ -358,7 +363,7 @@ def _create_session(self) -> Session:
358363

359364
# Sets the client side and server side SSL cert verification, if provided as properties.
360365
if ssl_config := self.properties.get(SSL):
361-
if ssl_ca_bundle := ssl_config.get(CA_BUNDLE):
366+
if (ssl_ca_bundle := ssl_config.get(CA_BUNDLE)) is not None:
362367
session.verify = ssl_ca_bundle
363368
if ssl_client := ssl_config.get(CLIENT):
364369
if all(k in ssl_client for k in (CERT, KEY)):

pyiceberg/catalog/rest/response.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
RESTError,
2929
ServerError,
3030
ServiceUnavailableError,
31+
TooManyRequestsError,
3132
UnauthorizedError,
3233
)
3334
from pyiceberg.typedef import IcebergBaseModel
@@ -79,6 +80,8 @@ def _handle_non_200_response(exc: HTTPError, error_handler: dict[int, type[Excep
7980
exception = RESTError
8081
elif code == 419:
8182
exception = AuthorizationExpiredError
83+
elif code == 429:
84+
exception = TooManyRequestsError
8285
elif code == 501:
8386
exception = NotImplementedError
8487
elif code == 503:

pyiceberg/cli/console.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def wrapper(*args: Any, **kwargs: Any): # type: ignore
6666
@click.option("--ugi")
6767
@click.option("--uri")
6868
@click.option("--credential")
69+
@click.option("--prefix")
6970
@click.pass_context
7071
def run(
7172
ctx: Context,
@@ -76,6 +77,7 @@ def run(
7677
ugi: str | None,
7778
uri: str | None,
7879
credential: str | None,
80+
prefix: str | None,
7981
) -> None:
8082
logging.basicConfig(
8183
level=getattr(logging, log_level.upper()),
@@ -89,6 +91,8 @@ def run(
8991
properties[URI] = uri
9092
if credential:
9193
properties["credential"] = credential
94+
if prefix:
95+
properties["prefix"] = prefix
9296

9397
ctx.ensure_object(dict)
9498
if output == "text":

pyiceberg/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ class AuthorizationExpiredError(RESTError):
8484
"""When the credentials are expired when performing an action on the REST catalog."""
8585

8686

87+
class TooManyRequestsError(RESTError):
88+
"""Raises when too many requests error is returned by the REST catalog."""
89+
90+
8791
class OAuthError(RESTError):
8892
"""Raises when there is an error with the OAuth call."""
8993

pyiceberg/table/update/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ class RemoveSchemasUpdate(IcebergBaseModel):
213213

214214
class SetPartitionStatisticsUpdate(IcebergBaseModel):
215215
action: Literal["set-partition-statistics"] = Field(default="set-partition-statistics")
216-
partition_statistics: PartitionStatisticsFile
216+
partition_statistics: PartitionStatisticsFile = Field(alias="partition-statistics")
217217

218218

219219
class RemovePartitionStatisticsUpdate(IcebergBaseModel):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ docs = [
126126
"mkdocs==1.6.1",
127127
"griffe==2.0.0",
128128
"jinja2==3.1.6",
129-
"mkdocstrings==1.0.2",
129+
"mkdocstrings==1.0.3",
130130
"mkdocstrings-python==2.0.2",
131131
"mkdocs-literate-nav==0.6.2",
132132
"mkdocs-autorefs==1.4.4",

tests/catalog/test_rest.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
OAUTH2_SERVER_URI,
3636
SNAPSHOT_LOADING_MODE,
3737
Capability,
38+
Endpoint,
39+
HttpMethod,
3840
RestCatalog,
3941
)
4042
from pyiceberg.exceptions import (
@@ -1641,6 +1643,19 @@ def test_update_namespace_properties_invalid_namespace(rest_mock: Mocker) -> Non
16411643
assert "Empty namespace identifier" in str(e.value)
16421644

16431645

1646+
def test_with_disabled_ssl_ca_bundle(rest_mock: Mocker) -> None:
1647+
# Given
1648+
catalog_properties = {
1649+
"uri": TEST_URI,
1650+
"token": TEST_TOKEN,
1651+
"ssl": {
1652+
"cabundle": False,
1653+
},
1654+
}
1655+
catalog = RestCatalog("rest", **catalog_properties) # type: ignore
1656+
assert catalog._session.verify is False
1657+
1658+
16441659
def test_request_session_with_ssl_ca_bundle(monkeypatch: pytest.MonkeyPatch) -> None:
16451660
# Given
16461661
catalog_properties = {
@@ -2351,3 +2366,27 @@ def test_table_uuid_check_on_refresh(rest_mock: Mocker, example_table_metadata_v
23512366
assert "Table UUID does not match" in str(exc_info.value)
23522367
assert f"current={original_uuid}" in str(exc_info.value)
23532368
assert f"refreshed={different_uuid}" in str(exc_info.value)
2369+
2370+
2371+
def test_endpoint_parsing_from_string_with_valid_http_method() -> None:
2372+
test_cases = [
2373+
("GET /v1/resource", HttpMethod.GET, "/v1/resource"),
2374+
("HEAD /v1/resource", HttpMethod.HEAD, "/v1/resource"),
2375+
("POST /v1/resource", HttpMethod.POST, "/v1/resource"),
2376+
("DELETE /v1/resource", HttpMethod.DELETE, "/v1/resource"),
2377+
("PUT /v1/resource", HttpMethod.PUT, "/v1/resource"),
2378+
("CONNECT /v1/resource", HttpMethod.CONNECT, "/v1/resource"),
2379+
("OPTIONS /v1/resource", HttpMethod.OPTIONS, "/v1/resource"),
2380+
("TRACE /v1/resource", HttpMethod.TRACE, "/v1/resource"),
2381+
("PATCH /v1/resource", HttpMethod.PATCH, "/v1/resource"),
2382+
]
2383+
2384+
for raw_string, http_method, path in test_cases:
2385+
endpoint = Endpoint.from_string(raw_string)
2386+
assert endpoint.http_method == http_method
2387+
assert endpoint.path == path
2388+
2389+
2390+
def test_endpoint_parsing_from_string_with_invalid_http_method() -> None:
2391+
with pytest.raises(ValueError, match="not a valid HttpMethod"):
2392+
Endpoint.from_string("INVALID /v1/resource")

tests/cli/test_console.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,3 +1029,18 @@ def test_log_level_cli_overrides_env(mocker: MockFixture) -> None:
10291029
mock_basicConfig.assert_called_once()
10301030
call_kwargs = mock_basicConfig.call_args[1]
10311031
assert call_kwargs["level"] == logging.ERROR
1032+
1033+
1034+
def test_prefix_cli_option_forwarded_to_catalog(mocker: MockFixture) -> None:
1035+
mock_basicConfig = mocker.patch("logging.basicConfig")
1036+
mock_catalog = MagicMock(spec=InMemoryCatalog)
1037+
mock_catalog.list_tables.return_value = []
1038+
mock_catalog.list_namespaces.return_value = []
1039+
mock_load_catalog = mocker.patch("pyiceberg.cli.console.load_catalog", return_value=mock_catalog)
1040+
1041+
runner = CliRunner()
1042+
result = runner.invoke(run, ["--catalog", "rest", "--uri", "https://example.invalid", "--prefix", "v1/ws", "list"])
1043+
1044+
assert result.exit_code == 0
1045+
mock_basicConfig.assert_called_once()
1046+
mock_load_catalog.assert_called_once_with("rest", uri="https://example.invalid", prefix="v1/ws")

0 commit comments

Comments
 (0)