Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvements related to --lifecycle-rule param handling #1036

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions b2/_internal/_cli/obj_loads.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,25 @@
import argparse
import io
import json
import logging
import sys
from typing import TypeVar

from b2sdk.v2 import get_b2sdk_doc_urls

try:
import pydantic
from pydantic import TypeAdapter, ValidationError

if sys.version_info < (3, 10):
raise ImportError('pydantic integration is not supported on python<3.10')
# we could support it partially with help of https://github.com/pydantic/pydantic/issues/7873
# but that creates yet another edge case, on old version of Python
except ImportError:
pydantic = None

logger = logging.getLogger(__name__)


def convert_error_to_human_readable(validation_exc: ValidationError) -> str:
buf = io.StringIO()
Expand All @@ -41,18 +50,33 @@ def describe_type(type_) -> str:

T = TypeVar('T')

_UNDEF = object()


def validated_loads(data: str, expected_type: type[T] | None = None) -> T:
val = _UNDEF
if expected_type is not None and pydantic is not None:
ta = TypeAdapter(expected_type)
expected_type = pydantic.with_config(pydantic.ConfigDict(extra="allow"))(expected_type)
try:
val = ta.validate_json(data)
except ValidationError as e:
errors = convert_error_to_human_readable(e)
raise argparse.ArgumentTypeError(
f'Invalid value inputted, expected {describe_type(expected_type)}, got {data!r}, more detail below:\n{errors}'
) from e
else:
ta = TypeAdapter(expected_type)
except TypeError:
# TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'
# This is thrown on python<3.10 even with eval_type_backport
logger.debug(
f'Failed to create TypeAdapter for {expected_type!r} using pydantic, falling back to json.loads',
exc_info=True
)
val = _UNDEF
else:
try:
val = ta.validate_json(data)
except ValidationError as e:
errors = convert_error_to_human_readable(e)
raise argparse.ArgumentTypeError(
f'Invalid value inputted, expected {describe_type(expected_type)}, got {data!r}, more detail below:\n{errors}'
) from e

if val is _UNDEF:
try:
val = json.loads(data)
except json.JSONDecodeError as e:
Expand Down
5 changes: 3 additions & 2 deletions b2/_internal/console_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,8 +835,9 @@ class LifecycleRulesMixin(Described):
"""
Use `--lifecycle-rule` to set lifecycle rule for the bucket.
Multiple rules can be specified by repeating the option.

`--lifecycle-rules` option is deprecated and cannot be used together with --lifecycle-rule.
All bucket lifecycle rules are set at once, so if you want to add a new rule,
you need to provide all existing rules.
Example: :code:`--lifecycle-rule '{{"daysFromHidingToDeleting": 1, "daysFromUploadingToHiding": null, "fileNamePrefix": "documents/"}}' --lifecycle-rule '{{"daysFromHidingToDeleting": 1, "daysFromUploadingToHiding": 7, "fileNamePrefix": "temporary/"}}'`
"""

@classmethod
Expand Down
1 change: 1 addition & 0 deletions changelog.d/+lifecycle_rule_validation.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix `--lifecycle-rule` validation on `python<3.10`.
1 change: 1 addition & 0 deletions changelog.d/432.doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `--lifecycle-rule` example to CLI `--help` and documentation.
31 changes: 30 additions & 1 deletion test/unit/_cli/test_obj_loads.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@
# License https://www.backblaze.com/using_b2_code.html
#
######################################################################
from __future__ import annotations

import argparse

import pytest

from b2._internal._cli.obj_loads import validated_loads
try:
from typing_extensions import TypedDict
except ImportError:
from typing import TypedDict

from b2._internal._cli.obj_loads import pydantic, validated_loads


@pytest.mark.parametrize(
Expand Down Expand Up @@ -46,3 +53,25 @@ def test_validated_loads(input_, expected_val):
def test_validated_loads__invalid_syntax(input_, error_msg):
with pytest.raises(argparse.ArgumentTypeError, match=error_msg):
validated_loads(input_)


@pytest.fixture
def typed_dict_cls():
class MyTypedDict(TypedDict):
a: int | None
b: str

return MyTypedDict


def test_validated_loads__typed_dict(typed_dict_cls):
input_ = '{"a": 1, "b": "2", "extra": null}'
expected_val = {"a": 1, "b": "2", "extra": None}
assert validated_loads(input_, typed_dict_cls) == expected_val


@pytest.mark.skipif(pydantic is None, reason="pydantic is not enabled")
def test_validated_loads__typed_dict_types_validation(typed_dict_cls):
input_ = '{"a": "abc", "b": 2}'
with pytest.raises(argparse.ArgumentTypeError):
validated_loads(input_, typed_dict_cls)