Skip to content

Commit

Permalink
Merge pull request Backblaze#1036 from reef-technologies/update_lifec…
Browse files Browse the repository at this point in the history
…ycle_rules_def

improvements related to `--lifecycle-rule` param handling
  • Loading branch information
mjurbanski-reef committed Jun 13, 2024
2 parents 104a0e6 + 27a953b commit 6f5d880
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 11 deletions.
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)

0 comments on commit 6f5d880

Please sign in to comment.