robot-dynamodb 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robot_dynamodb-0.1.0/.github/workflows/ci.yml +23 -0
- robot_dynamodb-0.1.0/.github/workflows/publish.yml +29 -0
- robot_dynamodb-0.1.0/PKG-INFO +70 -0
- robot_dynamodb-0.1.0/README.md +59 -0
- robot_dynamodb-0.1.0/pyproject.toml +33 -0
- robot_dynamodb-0.1.0/src/robot_dynamodb/__init__.py +3 -0
- robot_dynamodb-0.1.0/src/robot_dynamodb/library.py +161 -0
- robot_dynamodb-0.1.0/src/robot_dynamodb/py.typed +0 -0
- robot_dynamodb-0.1.0/tests/__init__.py +0 -0
- robot_dynamodb-0.1.0/tests/test_library.py +204 -0
- robot_dynamodb-0.1.0/uv.lock +191 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master, main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- uses: astral-sh/setup-uv@v4
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.14"
|
|
18
|
+
|
|
19
|
+
- name: Install dependencies
|
|
20
|
+
run: uv sync --all-groups
|
|
21
|
+
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: uv run pytest tests/ -v
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@v4
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.14"
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: uv sync
|
|
24
|
+
|
|
25
|
+
- name: Build
|
|
26
|
+
run: uv build
|
|
27
|
+
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robot-dynamodb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Robot Framework library for DynamoDB (LocalStack and AWS)
|
|
5
|
+
Author-email: Leandro Santiago Gomes <leandroluk@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Requires-Dist: boto3>=1.34.0
|
|
9
|
+
Requires-Dist: robotframework>=7.0.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# robot-dynamodb
|
|
13
|
+
|
|
14
|
+
Robot Framework library for DynamoDB. Works with LocalStack and AWS.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install robot-dynamodb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```robot
|
|
25
|
+
*** Settings ***
|
|
26
|
+
Library robot_dynamodb.DynamoDbLibrary
|
|
27
|
+
|
|
28
|
+
Suite Setup Connect To DynamoDB
|
|
29
|
+
... endpoint_url=http://localhost:4566
|
|
30
|
+
... region=sa-east-1
|
|
31
|
+
... table_prefix=dev_
|
|
32
|
+
... table_postfix=_v1
|
|
33
|
+
Suite Teardown Disconnect From DynamoDB
|
|
34
|
+
|
|
35
|
+
Test Setup Truncate DynamoDB Table orders
|
|
36
|
+
Test Teardown Truncate DynamoDB Table orders
|
|
37
|
+
|
|
38
|
+
*** Test Cases ***
|
|
39
|
+
Pedido deve ser persistido
|
|
40
|
+
Batch Write DynamoDB Items orders ${seed_items}
|
|
41
|
+
Item Should Exist In DynamoDB orders {"id": "123"}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Keywords
|
|
45
|
+
|
|
46
|
+
| Keyword | Descrição |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `Connect To DynamoDB` | Cria conexão boto3 |
|
|
49
|
+
| `Disconnect From DynamoDB` | Limpa estado |
|
|
50
|
+
| `Create DynamoDB Item` | Insere item (`put_item`) |
|
|
51
|
+
| `Get DynamoDB Item` | Busca por chave |
|
|
52
|
+
| `Update DynamoDB Item` | Atualiza atributos |
|
|
53
|
+
| `Delete DynamoDB Item` | Remove por chave |
|
|
54
|
+
| `Scan DynamoDB Table` | Lista todos os items |
|
|
55
|
+
| `Query DynamoDB Table` | Busca por partition key |
|
|
56
|
+
| `Truncate DynamoDB Table` | Deleta todos os items |
|
|
57
|
+
| `Batch Write DynamoDB Items` | Insere lista de items |
|
|
58
|
+
| `Item Should Exist In DynamoDB` | Falha se item não encontrado |
|
|
59
|
+
| `Item Should Not Exist In DynamoDB` | Falha se item encontrado |
|
|
60
|
+
|
|
61
|
+
## Connect To DynamoDB Parameters
|
|
62
|
+
|
|
63
|
+
| Parâmetro | Default | Descrição |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `endpoint_url` | — | URL do DynamoDB / LocalStack |
|
|
66
|
+
| `region` | `us-east-1` | AWS region |
|
|
67
|
+
| `table_prefix` | `""` | Prefixo do nome da tabela |
|
|
68
|
+
| `table_postfix` | `""` | Sufixo do nome da tabela |
|
|
69
|
+
| `aws_access_key_id` | `test` | AWS / LocalStack key |
|
|
70
|
+
| `aws_secret_access_key` | `test` | AWS / LocalStack secret |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# robot-dynamodb
|
|
2
|
+
|
|
3
|
+
Robot Framework library for DynamoDB. Works with LocalStack and AWS.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install robot-dynamodb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```robot
|
|
14
|
+
*** Settings ***
|
|
15
|
+
Library robot_dynamodb.DynamoDbLibrary
|
|
16
|
+
|
|
17
|
+
Suite Setup Connect To DynamoDB
|
|
18
|
+
... endpoint_url=http://localhost:4566
|
|
19
|
+
... region=sa-east-1
|
|
20
|
+
... table_prefix=dev_
|
|
21
|
+
... table_postfix=_v1
|
|
22
|
+
Suite Teardown Disconnect From DynamoDB
|
|
23
|
+
|
|
24
|
+
Test Setup Truncate DynamoDB Table orders
|
|
25
|
+
Test Teardown Truncate DynamoDB Table orders
|
|
26
|
+
|
|
27
|
+
*** Test Cases ***
|
|
28
|
+
Pedido deve ser persistido
|
|
29
|
+
Batch Write DynamoDB Items orders ${seed_items}
|
|
30
|
+
Item Should Exist In DynamoDB orders {"id": "123"}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Keywords
|
|
34
|
+
|
|
35
|
+
| Keyword | Descrição |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `Connect To DynamoDB` | Cria conexão boto3 |
|
|
38
|
+
| `Disconnect From DynamoDB` | Limpa estado |
|
|
39
|
+
| `Create DynamoDB Item` | Insere item (`put_item`) |
|
|
40
|
+
| `Get DynamoDB Item` | Busca por chave |
|
|
41
|
+
| `Update DynamoDB Item` | Atualiza atributos |
|
|
42
|
+
| `Delete DynamoDB Item` | Remove por chave |
|
|
43
|
+
| `Scan DynamoDB Table` | Lista todos os items |
|
|
44
|
+
| `Query DynamoDB Table` | Busca por partition key |
|
|
45
|
+
| `Truncate DynamoDB Table` | Deleta todos os items |
|
|
46
|
+
| `Batch Write DynamoDB Items` | Insere lista de items |
|
|
47
|
+
| `Item Should Exist In DynamoDB` | Falha se item não encontrado |
|
|
48
|
+
| `Item Should Not Exist In DynamoDB` | Falha se item encontrado |
|
|
49
|
+
|
|
50
|
+
## Connect To DynamoDB Parameters
|
|
51
|
+
|
|
52
|
+
| Parâmetro | Default | Descrição |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `endpoint_url` | — | URL do DynamoDB / LocalStack |
|
|
55
|
+
| `region` | `us-east-1` | AWS region |
|
|
56
|
+
| `table_prefix` | `""` | Prefixo do nome da tabela |
|
|
57
|
+
| `table_postfix` | `""` | Sufixo do nome da tabela |
|
|
58
|
+
| `aws_access_key_id` | `test` | AWS / LocalStack key |
|
|
59
|
+
| `aws_secret_access_key` | `test` | AWS / LocalStack secret |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "robot-dynamodb"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Robot Framework library for DynamoDB (LocalStack and AWS)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Leandro Santiago Gomes", email = "leandroluk@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
dependencies = [
|
|
12
|
+
"boto3>=1.34.0",
|
|
13
|
+
"robotframework>=7.0.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=8.0.0",
|
|
19
|
+
"pytest-mock>=3.14.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["hatchling"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/robot_dynamodb"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
target-version = "py314"
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from typing import Any, Protocol, cast
|
|
2
|
+
|
|
3
|
+
import boto3
|
|
4
|
+
from robot.api.deco import keyword, library
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DynamoDBTable(Protocol):
|
|
8
|
+
key_schema: list[dict[str, str]]
|
|
9
|
+
|
|
10
|
+
def put_item(self, Item: dict[str, Any], **kwargs: Any) -> dict[str, Any]: ...
|
|
11
|
+
def get_item(self, Key: dict[str, Any], **kwargs: Any) -> dict[str, Any]: ...
|
|
12
|
+
def update_item(self, Key: dict[str, Any], **kwargs: Any) -> dict[str, Any]: ...
|
|
13
|
+
def delete_item(self, Key: dict[str, Any], **kwargs: Any) -> dict[str, Any]: ...
|
|
14
|
+
def scan(self, **kwargs: Any) -> dict[str, Any]: ...
|
|
15
|
+
def query(self, **kwargs: Any) -> dict[str, Any]: ...
|
|
16
|
+
def batch_writer(self) -> Any: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DynamoDBResource(Protocol):
|
|
20
|
+
def Table(self, name: str) -> DynamoDBTable: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@library(scope="SUITE")
|
|
24
|
+
class DynamoDbLibrary:
|
|
25
|
+
_resource: DynamoDBResource | None
|
|
26
|
+
_table_prefix: str
|
|
27
|
+
_table_postfix: str
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self._resource = None
|
|
31
|
+
self._table_prefix = ""
|
|
32
|
+
self._table_postfix = ""
|
|
33
|
+
|
|
34
|
+
def _get_resource(self) -> DynamoDBResource:
|
|
35
|
+
if self._resource is None:
|
|
36
|
+
raise RuntimeError("Not connected. Call 'Connect To DynamoDB' first.")
|
|
37
|
+
return self._resource
|
|
38
|
+
|
|
39
|
+
def _table(self, table_name: str) -> DynamoDBTable:
|
|
40
|
+
return self._get_resource().Table(
|
|
41
|
+
f"{self._table_prefix}{table_name}{self._table_postfix}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@keyword
|
|
45
|
+
def connect_to_dynamodb(
|
|
46
|
+
self,
|
|
47
|
+
endpoint_url: str,
|
|
48
|
+
region: str = "us-east-1",
|
|
49
|
+
table_prefix: str = "",
|
|
50
|
+
table_postfix: str = "",
|
|
51
|
+
aws_access_key_id: str = "test",
|
|
52
|
+
aws_secret_access_key: str = "test",
|
|
53
|
+
) -> None:
|
|
54
|
+
self._table_prefix = table_prefix
|
|
55
|
+
self._table_postfix = table_postfix
|
|
56
|
+
self._resource = cast(
|
|
57
|
+
DynamoDBResource,
|
|
58
|
+
boto3.resource(
|
|
59
|
+
"dynamodb",
|
|
60
|
+
endpoint_url=endpoint_url,
|
|
61
|
+
region_name=region,
|
|
62
|
+
aws_access_key_id=aws_access_key_id,
|
|
63
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@keyword
|
|
68
|
+
def disconnect_from_dynamodb(self) -> None:
|
|
69
|
+
self._resource = None
|
|
70
|
+
self._table_prefix = ""
|
|
71
|
+
self._table_postfix = ""
|
|
72
|
+
|
|
73
|
+
@keyword
|
|
74
|
+
def create_dynamodb_item(self, table_name: str, item: dict[str, Any]) -> dict[str, Any]:
|
|
75
|
+
self._table(table_name).put_item(Item=item)
|
|
76
|
+
return item
|
|
77
|
+
|
|
78
|
+
@keyword
|
|
79
|
+
def get_dynamodb_item(self, table_name: str, key: dict[str, Any]) -> dict[str, Any] | None:
|
|
80
|
+
return self._table(table_name).get_item(Key=key).get("Item")
|
|
81
|
+
|
|
82
|
+
@keyword
|
|
83
|
+
def update_dynamodb_item(
|
|
84
|
+
self,
|
|
85
|
+
table_name: str,
|
|
86
|
+
key: dict[str, Any],
|
|
87
|
+
update_expression: str,
|
|
88
|
+
expression_attribute_values: dict[str, Any],
|
|
89
|
+
expression_attribute_names: dict[str, str] | None = None,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
kwargs: dict[str, Any] = {
|
|
92
|
+
"Key": key,
|
|
93
|
+
"UpdateExpression": update_expression,
|
|
94
|
+
"ExpressionAttributeValues": expression_attribute_values,
|
|
95
|
+
}
|
|
96
|
+
if expression_attribute_names:
|
|
97
|
+
kwargs["ExpressionAttributeNames"] = expression_attribute_names
|
|
98
|
+
return self._table(table_name).update_item(**kwargs)
|
|
99
|
+
|
|
100
|
+
@keyword
|
|
101
|
+
def delete_dynamodb_item(self, table_name: str, key: dict[str, Any]) -> None:
|
|
102
|
+
self._table(table_name).delete_item(Key=key)
|
|
103
|
+
|
|
104
|
+
@keyword
|
|
105
|
+
def scan_dynamodb_table(self, table_name: str) -> list[dict[str, Any]]:
|
|
106
|
+
return self._table(table_name).scan().get("Items", [])
|
|
107
|
+
|
|
108
|
+
@keyword
|
|
109
|
+
def query_dynamodb_table(
|
|
110
|
+
self,
|
|
111
|
+
table_name: str,
|
|
112
|
+
key_condition_expression: str,
|
|
113
|
+
expression_attribute_values: dict[str, Any],
|
|
114
|
+
expression_attribute_names: dict[str, str] | None = None,
|
|
115
|
+
) -> list[dict[str, Any]]:
|
|
116
|
+
kwargs: dict[str, Any] = {
|
|
117
|
+
"KeyConditionExpression": key_condition_expression,
|
|
118
|
+
"ExpressionAttributeValues": expression_attribute_values,
|
|
119
|
+
}
|
|
120
|
+
if expression_attribute_names:
|
|
121
|
+
kwargs["ExpressionAttributeNames"] = expression_attribute_names
|
|
122
|
+
return self._table(table_name).query(**kwargs).get("Items", [])
|
|
123
|
+
|
|
124
|
+
@keyword
|
|
125
|
+
def truncate_dynamodb_table(self, table_name: str) -> None:
|
|
126
|
+
table = self._table(table_name)
|
|
127
|
+
key_names = {k["AttributeName"] for k in table.key_schema}
|
|
128
|
+
items = table.scan().get("Items", [])
|
|
129
|
+
with table.batch_writer() as batch:
|
|
130
|
+
for item in items:
|
|
131
|
+
batch.delete_item(Key={k: v for k, v in item.items() if k in key_names})
|
|
132
|
+
|
|
133
|
+
@keyword
|
|
134
|
+
def batch_write_dynamodb_items(
|
|
135
|
+
self, table_name: str, items: list[dict[str, Any]]
|
|
136
|
+
) -> None:
|
|
137
|
+
table = self._table(table_name)
|
|
138
|
+
with table.batch_writer() as batch:
|
|
139
|
+
for item in items:
|
|
140
|
+
batch.put_item(Item=item)
|
|
141
|
+
|
|
142
|
+
@keyword
|
|
143
|
+
def item_should_exist_in_dynamodb(
|
|
144
|
+
self, table_name: str, key: dict[str, Any]
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
item = self.get_dynamodb_item(table_name, key)
|
|
147
|
+
if item is None:
|
|
148
|
+
full = f"{self._table_prefix}{table_name}{self._table_postfix}"
|
|
149
|
+
raise AssertionError(f"Item {key} not found in table '{full}'")
|
|
150
|
+
return item
|
|
151
|
+
|
|
152
|
+
@keyword
|
|
153
|
+
def item_should_not_exist_in_dynamodb(
|
|
154
|
+
self, table_name: str, key: dict[str, Any]
|
|
155
|
+
) -> None:
|
|
156
|
+
item = self.get_dynamodb_item(table_name, key)
|
|
157
|
+
if item is not None:
|
|
158
|
+
full = f"{self._table_prefix}{table_name}{self._table_postfix}"
|
|
159
|
+
raise AssertionError(
|
|
160
|
+
f"Item {key} found in table '{full}' but should not exist"
|
|
161
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import MagicMock
|
|
3
|
+
from robot_dynamodb.library import DynamoDbLibrary
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def lib(mocker) -> DynamoDbLibrary:
|
|
8
|
+
mock_resource = mocker.MagicMock()
|
|
9
|
+
mocker.patch("robot_dynamodb.library.boto3.resource", return_value=mock_resource)
|
|
10
|
+
instance = DynamoDbLibrary()
|
|
11
|
+
instance.connect_to_dynamodb(
|
|
12
|
+
"http://localhost:4566", table_prefix="test_", table_postfix="_v1"
|
|
13
|
+
)
|
|
14
|
+
return instance
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_table(lib) -> MagicMock:
|
|
19
|
+
return lib._resource.Table.return_value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestConnection:
|
|
23
|
+
def test_connect_passes_correct_args(self, mocker):
|
|
24
|
+
mock_boto = mocker.patch("robot_dynamodb.library.boto3.resource")
|
|
25
|
+
lib = DynamoDbLibrary()
|
|
26
|
+
lib.connect_to_dynamodb("http://localhost:4566", region="sa-east-1")
|
|
27
|
+
mock_boto.assert_called_once_with(
|
|
28
|
+
"dynamodb",
|
|
29
|
+
endpoint_url="http://localhost:4566",
|
|
30
|
+
region_name="sa-east-1",
|
|
31
|
+
aws_access_key_id="test",
|
|
32
|
+
aws_secret_access_key="test",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def test_connect_stores_prefix_and_postfix(self, mocker):
|
|
36
|
+
mocker.patch("robot_dynamodb.library.boto3.resource")
|
|
37
|
+
lib = DynamoDbLibrary()
|
|
38
|
+
lib.connect_to_dynamodb(
|
|
39
|
+
"http://localhost:4566", table_prefix="dev_", table_postfix="_v2"
|
|
40
|
+
)
|
|
41
|
+
assert lib._table_prefix == "dev_"
|
|
42
|
+
assert lib._table_postfix == "_v2"
|
|
43
|
+
|
|
44
|
+
def test_disconnect_clears_state(self, lib):
|
|
45
|
+
lib.disconnect_from_dynamodb()
|
|
46
|
+
assert lib._resource is None
|
|
47
|
+
assert lib._table_prefix == ""
|
|
48
|
+
assert lib._table_postfix == ""
|
|
49
|
+
|
|
50
|
+
def test_get_resource_without_connect_raises(self):
|
|
51
|
+
lib = DynamoDbLibrary()
|
|
52
|
+
with pytest.raises(RuntimeError, match="Not connected"):
|
|
53
|
+
lib._get_resource()
|
|
54
|
+
|
|
55
|
+
def test_table_name_uses_prefix_and_postfix(self, lib):
|
|
56
|
+
lib._table("orders")
|
|
57
|
+
lib._resource.Table.assert_called_with("test_orders_v1")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestCRUD:
|
|
61
|
+
def test_create_item_calls_put_and_returns_item(self, lib, mock_table):
|
|
62
|
+
item = {"id": "1", "name": "test"}
|
|
63
|
+
result = lib.create_dynamodb_item("orders", item)
|
|
64
|
+
lib._resource.Table.assert_called_with("test_orders_v1")
|
|
65
|
+
mock_table.put_item.assert_called_once_with(Item=item)
|
|
66
|
+
assert result == item
|
|
67
|
+
|
|
68
|
+
def test_get_item_returns_item(self, lib, mock_table):
|
|
69
|
+
mock_table.get_item.return_value = {"Item": {"id": "1"}}
|
|
70
|
+
result = lib.get_dynamodb_item("orders", {"id": "1"})
|
|
71
|
+
mock_table.get_item.assert_called_once_with(Key={"id": "1"})
|
|
72
|
+
assert result == {"id": "1"}
|
|
73
|
+
|
|
74
|
+
def test_get_item_returns_none_when_missing(self, lib, mock_table):
|
|
75
|
+
mock_table.get_item.return_value = {}
|
|
76
|
+
assert lib.get_dynamodb_item("orders", {"id": "999"}) is None
|
|
77
|
+
|
|
78
|
+
def test_update_item_without_names(self, lib, mock_table):
|
|
79
|
+
mock_table.update_item.return_value = {}
|
|
80
|
+
lib.update_dynamodb_item("orders", {"id": "1"}, "SET #s = :s", {":s": "shipped"})
|
|
81
|
+
mock_table.update_item.assert_called_once_with(
|
|
82
|
+
Key={"id": "1"},
|
|
83
|
+
UpdateExpression="SET #s = :s",
|
|
84
|
+
ExpressionAttributeValues={":s": "shipped"},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def test_update_item_with_names(self, lib, mock_table):
|
|
88
|
+
mock_table.update_item.return_value = {}
|
|
89
|
+
lib.update_dynamodb_item(
|
|
90
|
+
"orders", {"id": "1"}, "SET #s = :s", {":s": "ok"}, {"#s": "status"}
|
|
91
|
+
)
|
|
92
|
+
mock_table.update_item.assert_called_once_with(
|
|
93
|
+
Key={"id": "1"},
|
|
94
|
+
UpdateExpression="SET #s = :s",
|
|
95
|
+
ExpressionAttributeValues={":s": "ok"},
|
|
96
|
+
ExpressionAttributeNames={"#s": "status"},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def test_delete_item(self, lib, mock_table):
|
|
100
|
+
lib.delete_dynamodb_item("orders", {"id": "1"})
|
|
101
|
+
mock_table.delete_item.assert_called_once_with(Key={"id": "1"})
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestScanQuery:
|
|
105
|
+
def test_scan_returns_items(self, lib, mock_table):
|
|
106
|
+
mock_table.scan.return_value = {"Items": [{"id": "1"}, {"id": "2"}]}
|
|
107
|
+
result = lib.scan_dynamodb_table("orders")
|
|
108
|
+
mock_table.scan.assert_called_once_with()
|
|
109
|
+
assert result == [{"id": "1"}, {"id": "2"}]
|
|
110
|
+
|
|
111
|
+
def test_scan_returns_empty_list_when_no_items(self, lib, mock_table):
|
|
112
|
+
mock_table.scan.return_value = {}
|
|
113
|
+
assert lib.scan_dynamodb_table("orders") == []
|
|
114
|
+
|
|
115
|
+
def test_query_without_names(self, lib, mock_table):
|
|
116
|
+
mock_table.query.return_value = {"Items": [{"id": "1"}]}
|
|
117
|
+
result = lib.query_dynamodb_table("orders", "id = :id", {":id": "1"})
|
|
118
|
+
mock_table.query.assert_called_once_with(
|
|
119
|
+
KeyConditionExpression="id = :id",
|
|
120
|
+
ExpressionAttributeValues={":id": "1"},
|
|
121
|
+
)
|
|
122
|
+
assert result == [{"id": "1"}]
|
|
123
|
+
|
|
124
|
+
def test_query_with_names(self, lib, mock_table):
|
|
125
|
+
mock_table.query.return_value = {"Items": []}
|
|
126
|
+
lib.query_dynamodb_table("orders", "#id = :id", {":id": "1"}, {"#id": "id"})
|
|
127
|
+
mock_table.query.assert_called_once_with(
|
|
128
|
+
KeyConditionExpression="#id = :id",
|
|
129
|
+
ExpressionAttributeValues={":id": "1"},
|
|
130
|
+
ExpressionAttributeNames={"#id": "id"},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def test_query_returns_empty_list_when_no_items(self, lib, mock_table):
|
|
134
|
+
mock_table.query.return_value = {}
|
|
135
|
+
assert lib.query_dynamodb_table("orders", "id = :id", {":id": "x"}) == []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TestBulkOps:
|
|
139
|
+
def test_truncate_deletes_all_items_by_key(self, lib, mock_table):
|
|
140
|
+
mock_table.scan.return_value = {
|
|
141
|
+
"Items": [{"id": "1", "extra": "x"}, {"id": "2", "extra": "y"}]
|
|
142
|
+
}
|
|
143
|
+
mock_table.key_schema = [{"AttributeName": "id", "KeyType": "HASH"}]
|
|
144
|
+
batch = MagicMock()
|
|
145
|
+
mock_table.batch_writer.return_value.__enter__ = MagicMock(return_value=batch)
|
|
146
|
+
mock_table.batch_writer.return_value.__exit__ = MagicMock(return_value=False)
|
|
147
|
+
|
|
148
|
+
lib.truncate_dynamodb_table("orders")
|
|
149
|
+
|
|
150
|
+
assert batch.delete_item.call_count == 2
|
|
151
|
+
batch.delete_item.assert_any_call(Key={"id": "1"})
|
|
152
|
+
batch.delete_item.assert_any_call(Key={"id": "2"})
|
|
153
|
+
|
|
154
|
+
def test_truncate_empty_table_does_nothing(self, lib, mock_table):
|
|
155
|
+
mock_table.scan.return_value = {"Items": []}
|
|
156
|
+
mock_table.key_schema = [{"AttributeName": "id", "KeyType": "HASH"}]
|
|
157
|
+
batch = MagicMock()
|
|
158
|
+
mock_table.batch_writer.return_value.__enter__ = MagicMock(return_value=batch)
|
|
159
|
+
mock_table.batch_writer.return_value.__exit__ = MagicMock(return_value=False)
|
|
160
|
+
|
|
161
|
+
lib.truncate_dynamodb_table("orders")
|
|
162
|
+
|
|
163
|
+
batch.delete_item.assert_not_called()
|
|
164
|
+
|
|
165
|
+
def test_batch_write_puts_all_items(self, lib, mock_table):
|
|
166
|
+
batch = MagicMock()
|
|
167
|
+
mock_table.batch_writer.return_value.__enter__ = MagicMock(return_value=batch)
|
|
168
|
+
mock_table.batch_writer.return_value.__exit__ = MagicMock(return_value=False)
|
|
169
|
+
|
|
170
|
+
lib.batch_write_dynamodb_items("orders", [{"id": "1"}, {"id": "2"}])
|
|
171
|
+
|
|
172
|
+
assert batch.put_item.call_count == 2
|
|
173
|
+
batch.put_item.assert_any_call(Item={"id": "1"})
|
|
174
|
+
batch.put_item.assert_any_call(Item={"id": "2"})
|
|
175
|
+
|
|
176
|
+
def test_batch_write_empty_list_does_nothing(self, lib, mock_table):
|
|
177
|
+
batch = MagicMock()
|
|
178
|
+
mock_table.batch_writer.return_value.__enter__ = MagicMock(return_value=batch)
|
|
179
|
+
mock_table.batch_writer.return_value.__exit__ = MagicMock(return_value=False)
|
|
180
|
+
|
|
181
|
+
lib.batch_write_dynamodb_items("orders", [])
|
|
182
|
+
|
|
183
|
+
batch.put_item.assert_not_called()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestAssertions:
|
|
187
|
+
def test_item_should_exist_returns_item_when_found(self, lib, mock_table):
|
|
188
|
+
mock_table.get_item.return_value = {"Item": {"id": "1"}}
|
|
189
|
+
result = lib.item_should_exist_in_dynamodb("orders", {"id": "1"})
|
|
190
|
+
assert result == {"id": "1"}
|
|
191
|
+
|
|
192
|
+
def test_item_should_exist_raises_when_missing(self, lib, mock_table):
|
|
193
|
+
mock_table.get_item.return_value = {}
|
|
194
|
+
with pytest.raises(AssertionError, match="not found"):
|
|
195
|
+
lib.item_should_exist_in_dynamodb("orders", {"id": "999"})
|
|
196
|
+
|
|
197
|
+
def test_item_should_not_exist_passes_when_missing(self, lib, mock_table):
|
|
198
|
+
mock_table.get_item.return_value = {}
|
|
199
|
+
lib.item_should_not_exist_in_dynamodb("orders", {"id": "999"})
|
|
200
|
+
|
|
201
|
+
def test_item_should_not_exist_raises_when_found(self, lib, mock_table):
|
|
202
|
+
mock_table.get_item.return_value = {"Item": {"id": "1"}}
|
|
203
|
+
with pytest.raises(AssertionError, match="should not exist"):
|
|
204
|
+
lib.item_should_not_exist_in_dynamodb("orders", {"id": "1"})
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.14"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "boto3"
|
|
7
|
+
version = "1.43.25"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "botocore" },
|
|
11
|
+
{ name = "jmespath" },
|
|
12
|
+
{ name = "s3transfer" },
|
|
13
|
+
]
|
|
14
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b5/d0/9154ca8bd6051a55102a5c8bb077b15959681a5f4dcf614a423afaefdf83/boto3-1.43.25.tar.gz", hash = "sha256:91f3deb0dfc32403a187f57c19a78dc8e38d25ee2967a511fa8905acf00cc074", size = 113169, upload-time = "2026-06-08T19:49:27.877Z" }
|
|
15
|
+
wheels = [
|
|
16
|
+
{ url = "https://files.pythonhosted.org/packages/82/89/d27be8bb3dc72a6d1d1d8f9e039e89ae86827c05c47385d78a1d964571f4/boto3-1.43.25-py3-none-any.whl", hash = "sha256:e4e2515533a1593eb2302e9eda5baf4e1510abb219b41dd0db4696cc21bc5039", size = 140538, upload-time = "2026-06-08T19:49:25.427Z" },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[[package]]
|
|
20
|
+
name = "botocore"
|
|
21
|
+
version = "1.43.25"
|
|
22
|
+
source = { registry = "https://pypi.org/simple" }
|
|
23
|
+
dependencies = [
|
|
24
|
+
{ name = "jmespath" },
|
|
25
|
+
{ name = "python-dateutil" },
|
|
26
|
+
{ name = "urllib3" },
|
|
27
|
+
]
|
|
28
|
+
sdist = { url = "https://files.pythonhosted.org/packages/ad/03/9dc102506c3ebc3758a9e8602e7dafb78789993bcac5daa82398b56ae884/botocore-1.43.25.tar.gz", hash = "sha256:faab543ca6ae6f8fdc5f6318240bebfb8c05cd25823715fe02aad7edf0c4b383", size = 15478403, upload-time = "2026-06-08T19:49:13.505Z" }
|
|
29
|
+
wheels = [
|
|
30
|
+
{ url = "https://files.pythonhosted.org/packages/41/a5/6ceef332b18c348be9ec26aeff0da5b3f7ea046bf3fc654feecf6974c4d9/botocore-1.43.25-py3-none-any.whl", hash = "sha256:ef1d210ac9085ea0e5fc6ad2e63f0c7e97103c74f52156bf944289aaf6278d1b", size = 15161721, upload-time = "2026-06-08T19:49:08.751Z" },
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[[package]]
|
|
34
|
+
name = "colorama"
|
|
35
|
+
version = "0.4.6"
|
|
36
|
+
source = { registry = "https://pypi.org/simple" }
|
|
37
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
38
|
+
wheels = [
|
|
39
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[[package]]
|
|
43
|
+
name = "iniconfig"
|
|
44
|
+
version = "2.3.0"
|
|
45
|
+
source = { registry = "https://pypi.org/simple" }
|
|
46
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
47
|
+
wheels = [
|
|
48
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[[package]]
|
|
52
|
+
name = "jmespath"
|
|
53
|
+
version = "1.1.0"
|
|
54
|
+
source = { registry = "https://pypi.org/simple" }
|
|
55
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
|
|
56
|
+
wheels = [
|
|
57
|
+
{ url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[[package]]
|
|
61
|
+
name = "packaging"
|
|
62
|
+
version = "26.2"
|
|
63
|
+
source = { registry = "https://pypi.org/simple" }
|
|
64
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
|
65
|
+
wheels = [
|
|
66
|
+
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[[package]]
|
|
70
|
+
name = "pluggy"
|
|
71
|
+
version = "1.6.0"
|
|
72
|
+
source = { registry = "https://pypi.org/simple" }
|
|
73
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
74
|
+
wheels = [
|
|
75
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[[package]]
|
|
79
|
+
name = "pygments"
|
|
80
|
+
version = "2.20.0"
|
|
81
|
+
source = { registry = "https://pypi.org/simple" }
|
|
82
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
83
|
+
wheels = [
|
|
84
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
[[package]]
|
|
88
|
+
name = "pytest"
|
|
89
|
+
version = "9.0.3"
|
|
90
|
+
source = { registry = "https://pypi.org/simple" }
|
|
91
|
+
dependencies = [
|
|
92
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
93
|
+
{ name = "iniconfig" },
|
|
94
|
+
{ name = "packaging" },
|
|
95
|
+
{ name = "pluggy" },
|
|
96
|
+
{ name = "pygments" },
|
|
97
|
+
]
|
|
98
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
|
99
|
+
wheels = [
|
|
100
|
+
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
[[package]]
|
|
104
|
+
name = "pytest-mock"
|
|
105
|
+
version = "3.15.1"
|
|
106
|
+
source = { registry = "https://pypi.org/simple" }
|
|
107
|
+
dependencies = [
|
|
108
|
+
{ name = "pytest" },
|
|
109
|
+
]
|
|
110
|
+
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
|
|
111
|
+
wheels = [
|
|
112
|
+
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
[[package]]
|
|
116
|
+
name = "python-dateutil"
|
|
117
|
+
version = "2.9.0.post0"
|
|
118
|
+
source = { registry = "https://pypi.org/simple" }
|
|
119
|
+
dependencies = [
|
|
120
|
+
{ name = "six" },
|
|
121
|
+
]
|
|
122
|
+
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
|
123
|
+
wheels = [
|
|
124
|
+
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[[package]]
|
|
128
|
+
name = "robot-dynamodb"
|
|
129
|
+
version = "0.1.0"
|
|
130
|
+
source = { editable = "." }
|
|
131
|
+
dependencies = [
|
|
132
|
+
{ name = "boto3" },
|
|
133
|
+
{ name = "robotframework" },
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[package.dev-dependencies]
|
|
137
|
+
dev = [
|
|
138
|
+
{ name = "pytest" },
|
|
139
|
+
{ name = "pytest-mock" },
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
[package.metadata]
|
|
143
|
+
requires-dist = [
|
|
144
|
+
{ name = "boto3", specifier = ">=1.34.0" },
|
|
145
|
+
{ name = "robotframework", specifier = ">=7.0.0" },
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
[package.metadata.requires-dev]
|
|
149
|
+
dev = [
|
|
150
|
+
{ name = "pytest", specifier = ">=8.0.0" },
|
|
151
|
+
{ name = "pytest-mock", specifier = ">=3.14.0" },
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
[[package]]
|
|
155
|
+
name = "robotframework"
|
|
156
|
+
version = "7.4.2"
|
|
157
|
+
source = { registry = "https://pypi.org/simple" }
|
|
158
|
+
sdist = { url = "https://files.pythonhosted.org/packages/19/f3/ad51daf85d95848831601851598640f951a47a9f9de88039235cf58c5bb9/robotframework-7.4.2.tar.gz", hash = "sha256:1c934e7f43600de407860cd2bd2fdc41adad4a4a785d8b46b1ed485fdc0f6c9f", size = 654405, upload-time = "2026-03-03T16:28:54.899Z" }
|
|
159
|
+
wheels = [
|
|
160
|
+
{ url = "https://files.pythonhosted.org/packages/ef/35/fd2385b15f6d814f1801bcbd3d54b4c61a1bfc3a1a0fe023dc15551c5fe4/robotframework-7.4.2-py3-none-any.whl", hash = "sha256:6e80f84cdc997bdde2abb6b729ac3531457ecf6d2e41abfb87a541877ab367bf", size = 807056, upload-time = "2026-03-03T16:28:51.638Z" },
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
[[package]]
|
|
164
|
+
name = "s3transfer"
|
|
165
|
+
version = "0.18.0"
|
|
166
|
+
source = { registry = "https://pypi.org/simple" }
|
|
167
|
+
dependencies = [
|
|
168
|
+
{ name = "botocore" },
|
|
169
|
+
]
|
|
170
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e0/1f/12417f7f493fc45e1f9fd5d4a9b6c125cf8d2cf3f8ddbdfab3e76406e9d6/s3transfer-0.18.0.tar.gz", hash = "sha256:3760b8b7ec1315da54048b2d626276732bee4300d054d492d4e1d43e20d4ecbd", size = 160560, upload-time = "2026-05-28T19:39:09.124Z" }
|
|
171
|
+
wheels = [
|
|
172
|
+
{ url = "https://files.pythonhosted.org/packages/2b/58/a58fc997655386daa2e25784e30c288aa3e3819e401f77029ee4899fb55a/s3transfer-0.18.0-py3-none-any.whl", hash = "sha256:239c13b09e65ad0346e1be7348b8a202dcad44ac7ea7c6eb858fc881dce739b6", size = 88572, upload-time = "2026-05-28T19:39:07.999Z" },
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
[[package]]
|
|
176
|
+
name = "six"
|
|
177
|
+
version = "1.17.0"
|
|
178
|
+
source = { registry = "https://pypi.org/simple" }
|
|
179
|
+
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
|
180
|
+
wheels = [
|
|
181
|
+
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
[[package]]
|
|
185
|
+
name = "urllib3"
|
|
186
|
+
version = "2.7.0"
|
|
187
|
+
source = { registry = "https://pypi.org/simple" }
|
|
188
|
+
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
|
189
|
+
wheels = [
|
|
190
|
+
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
|
191
|
+
]
|