b24api 0.4.0__tar.gz → 0.5.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.
- {b24api-0.4.0/b24api.egg-info → b24api-0.5.0}/PKG-INFO +31 -18
- {b24api-0.4.0 → b24api-0.5.0}/README.md +26 -14
- b24api-0.5.0/b24api/__init__.py +4 -0
- b24api-0.5.0/b24api/async_api.py +221 -0
- b24api-0.5.0/b24api/base_api.py +291 -0
- {b24api-0.4.0 → b24api-0.5.0}/b24api/entity.py +3 -1
- {b24api-0.4.0 → b24api-0.5.0}/b24api/error.py +4 -9
- {b24api-0.4.0 → b24api-0.5.0}/b24api/settings.py +2 -0
- b24api-0.5.0/b24api/sync_api.py +215 -0
- {b24api-0.4.0 → b24api-0.5.0/b24api.egg-info}/PKG-INFO +31 -18
- {b24api-0.4.0 → b24api-0.5.0}/b24api.egg-info/SOURCES.txt +3 -1
- b24api-0.5.0/b24api.egg-info/requires.txt +6 -0
- {b24api-0.4.0 → b24api-0.5.0}/pyproject.toml +9 -8
- b24api-0.4.0/b24api/__init__.py +0 -3
- b24api-0.4.0/b24api/api.py +0 -392
- b24api-0.4.0/b24api.egg-info/requires.txt +0 -5
- {b24api-0.4.0 → b24api-0.5.0}/LICENSE +0 -0
- {b24api-0.4.0 → b24api-0.5.0}/MANIFEST.in +0 -0
- {b24api-0.4.0 → b24api-0.5.0}/b24api/query.py +0 -0
- {b24api-0.4.0 → b24api-0.5.0}/b24api/type.py +0 -0
- {b24api-0.4.0 → b24api-0.5.0}/b24api.egg-info/dependency_links.txt +0 -0
- {b24api-0.4.0 → b24api-0.5.0}/b24api.egg-info/top_level.txt +0 -0
- {b24api-0.4.0 → b24api-0.5.0}/setup.cfg +0 -0
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: b24api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Bitrix24 API
|
|
5
|
-
Requires-Python: >=3.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Dist: fast-depends>=2.4.12
|
|
9
9
|
Requires-Dist: httpx[http2]>=0.28.1
|
|
10
|
-
Requires-Dist: pydantic>=2.
|
|
11
|
-
Requires-Dist: pydantic-settings>=2.
|
|
10
|
+
Requires-Dist: pydantic>=2.11.7
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.10.1
|
|
12
|
+
Requires-Dist: pytest-asyncio>=1.1.0
|
|
12
13
|
Requires-Dist: tenacity>=9.1.2
|
|
13
14
|
Dynamic: license-file
|
|
14
15
|
|
|
@@ -18,19 +19,31 @@ Low-level API client with multiple strategies for lists gathering.
|
|
|
18
19
|
All methods support retries.
|
|
19
20
|
|
|
20
21
|
## Regular call (any method)
|
|
22
|
+
|
|
21
23
|
```python
|
|
22
|
-
from b24api import
|
|
24
|
+
from b24api import SyncBitrix24
|
|
23
25
|
|
|
24
|
-
b24 =
|
|
26
|
+
b24 = SyncBitrix24()
|
|
25
27
|
result = b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
|
|
26
28
|
print(result)
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
or the same with async
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from b24api import AsyncBitrix24
|
|
35
|
+
|
|
36
|
+
b24 = AsyncBitrix24()
|
|
37
|
+
result = await b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
|
|
38
|
+
print(result)
|
|
39
|
+
```
|
|
40
|
+
|
|
29
41
|
## Batch call (any method, no limit)
|
|
42
|
+
|
|
30
43
|
```python
|
|
31
|
-
from b24api import
|
|
44
|
+
from b24api import SyncBitrix24
|
|
32
45
|
|
|
33
|
-
b24 =
|
|
46
|
+
b24 = SyncBitrix24()
|
|
34
47
|
|
|
35
48
|
requests = ({"method": "user.update", "parameters": {"ID": u, "UF_SKYPE": ""}} for u in range(1000))
|
|
36
49
|
result = b24.batch(requests)
|
|
@@ -42,9 +55,9 @@ Applicable to list methods with `start=<offset>` support.
|
|
|
42
55
|
Fetches list chunks one by one.
|
|
43
56
|
|
|
44
57
|
```python
|
|
45
|
-
from b24api import
|
|
58
|
+
from b24api import SyncBitrix24
|
|
46
59
|
|
|
47
|
-
b24 =
|
|
60
|
+
b24 = SyncBitrix24()
|
|
48
61
|
result = b24.list_sequential({"method": "user.get"})
|
|
49
62
|
print(list(result))
|
|
50
63
|
```
|
|
@@ -52,12 +65,12 @@ print(list(result))
|
|
|
52
65
|
## Gathering full list (faster)
|
|
53
66
|
Applicable to list methods with `start=<offset>` support.
|
|
54
67
|
Fetches first list chunk with regular call, then fetches other chunks with `batch`.
|
|
55
|
-
Approximately 3 times faster then `list_sequential`.
|
|
68
|
+
Approximately 3 times faster then `list_sequential`.
|
|
56
69
|
|
|
57
70
|
```python
|
|
58
|
-
from b24api import
|
|
71
|
+
from b24api import SyncBitrix24
|
|
59
72
|
|
|
60
|
-
b24 =
|
|
73
|
+
b24 = SyncBitrix24()
|
|
61
74
|
result = b24.list_batched({"method": "user.get"})
|
|
62
75
|
print(list(result))
|
|
63
76
|
```
|
|
@@ -65,12 +78,12 @@ print(list(result))
|
|
|
65
78
|
## Gathering full list (fastest)
|
|
66
79
|
Applicable to list methods with `filter={<parameters>}` support.
|
|
67
80
|
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
68
|
-
Approximately 2 times faster then `list_batched`.
|
|
81
|
+
Approximately 2 times faster then `list_batched`.
|
|
69
82
|
|
|
70
83
|
```python
|
|
71
|
-
from b24api import
|
|
84
|
+
from b24api import SyncBitrix24
|
|
72
85
|
|
|
73
|
-
b24 =
|
|
86
|
+
b24 = SyncBitrix24()
|
|
74
87
|
result = b24.list_batched_no_count({"method": "user.get"})
|
|
75
88
|
print(list(result))
|
|
76
89
|
```
|
|
@@ -80,9 +93,9 @@ Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}`
|
|
|
80
93
|
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
81
94
|
|
|
82
95
|
```python
|
|
83
|
-
from b24api import
|
|
96
|
+
from b24api import SyncBitrix24
|
|
84
97
|
|
|
85
|
-
b24 =
|
|
98
|
+
b24 = SyncBitrix24()
|
|
86
99
|
deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
|
|
87
100
|
filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
|
|
88
101
|
result = b24.reference_batched_no_count(
|
|
@@ -4,19 +4,31 @@ Low-level API client with multiple strategies for lists gathering.
|
|
|
4
4
|
All methods support retries.
|
|
5
5
|
|
|
6
6
|
## Regular call (any method)
|
|
7
|
+
|
|
7
8
|
```python
|
|
8
|
-
from b24api import
|
|
9
|
+
from b24api import SyncBitrix24
|
|
9
10
|
|
|
10
|
-
b24 =
|
|
11
|
+
b24 = SyncBitrix24()
|
|
11
12
|
result = b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
|
|
12
13
|
print(result)
|
|
13
14
|
```
|
|
14
15
|
|
|
16
|
+
or the same with async
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from b24api import AsyncBitrix24
|
|
20
|
+
|
|
21
|
+
b24 = AsyncBitrix24()
|
|
22
|
+
result = await b24.call({"method": "user.access", "parameters": {"ACCESS": ["G2", "AU"]}})
|
|
23
|
+
print(result)
|
|
24
|
+
```
|
|
25
|
+
|
|
15
26
|
## Batch call (any method, no limit)
|
|
27
|
+
|
|
16
28
|
```python
|
|
17
|
-
from b24api import
|
|
29
|
+
from b24api import SyncBitrix24
|
|
18
30
|
|
|
19
|
-
b24 =
|
|
31
|
+
b24 = SyncBitrix24()
|
|
20
32
|
|
|
21
33
|
requests = ({"method": "user.update", "parameters": {"ID": u, "UF_SKYPE": ""}} for u in range(1000))
|
|
22
34
|
result = b24.batch(requests)
|
|
@@ -28,9 +40,9 @@ Applicable to list methods with `start=<offset>` support.
|
|
|
28
40
|
Fetches list chunks one by one.
|
|
29
41
|
|
|
30
42
|
```python
|
|
31
|
-
from b24api import
|
|
43
|
+
from b24api import SyncBitrix24
|
|
32
44
|
|
|
33
|
-
b24 =
|
|
45
|
+
b24 = SyncBitrix24()
|
|
34
46
|
result = b24.list_sequential({"method": "user.get"})
|
|
35
47
|
print(list(result))
|
|
36
48
|
```
|
|
@@ -38,12 +50,12 @@ print(list(result))
|
|
|
38
50
|
## Gathering full list (faster)
|
|
39
51
|
Applicable to list methods with `start=<offset>` support.
|
|
40
52
|
Fetches first list chunk with regular call, then fetches other chunks with `batch`.
|
|
41
|
-
Approximately 3 times faster then `list_sequential`.
|
|
53
|
+
Approximately 3 times faster then `list_sequential`.
|
|
42
54
|
|
|
43
55
|
```python
|
|
44
|
-
from b24api import
|
|
56
|
+
from b24api import SyncBitrix24
|
|
45
57
|
|
|
46
|
-
b24 =
|
|
58
|
+
b24 = SyncBitrix24()
|
|
47
59
|
result = b24.list_batched({"method": "user.get"})
|
|
48
60
|
print(list(result))
|
|
49
61
|
```
|
|
@@ -51,12 +63,12 @@ print(list(result))
|
|
|
51
63
|
## Gathering full list (fastest)
|
|
52
64
|
Applicable to list methods with `filter={<parameters>}` support.
|
|
53
65
|
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
54
|
-
Approximately 2 times faster then `list_batched`.
|
|
66
|
+
Approximately 2 times faster then `list_batched`.
|
|
55
67
|
|
|
56
68
|
```python
|
|
57
|
-
from b24api import
|
|
69
|
+
from b24api import SyncBitrix24
|
|
58
70
|
|
|
59
|
-
b24 =
|
|
71
|
+
b24 = SyncBitrix24()
|
|
60
72
|
result = b24.list_batched_no_count({"method": "user.get"})
|
|
61
73
|
print(list(result))
|
|
62
74
|
```
|
|
@@ -66,9 +78,9 @@ Applicable to list methods with `select=[<fields>]` and `filter={<parameters>}`
|
|
|
66
78
|
Fetches first and last list chunk with batch call, then fetches other chunks with `batch`. Doesn't use counting (`start=-1`).
|
|
67
79
|
|
|
68
80
|
```python
|
|
69
|
-
from b24api import
|
|
81
|
+
from b24api import SyncBitrix24
|
|
70
82
|
|
|
71
|
-
b24 =
|
|
83
|
+
b24 = SyncBitrix24()
|
|
72
84
|
deal_ids = [1, 2, 3] # deals IDs (e.g. from "crm.deal.list" call)
|
|
73
85
|
filter_updates = ({"=ENTITY_ID": i} for i in deal_ids)
|
|
74
86
|
result = b24.reference_batched_no_count(
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator, Iterable
|
|
2
|
+
from itertools import batched, islice
|
|
3
|
+
from operator import itemgetter
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from httpx import AsyncClient
|
|
7
|
+
from tenacity import AsyncRetrying
|
|
8
|
+
|
|
9
|
+
from b24api.base_api import BaseBitrix24
|
|
10
|
+
from b24api.entity import ApiTypes, ListRequest, Request, Response
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncBitrix24(BaseBitrix24):
|
|
14
|
+
@staticmethod
|
|
15
|
+
def _http() -> type[AsyncClient]:
|
|
16
|
+
return AsyncClient
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _retry() -> type[AsyncRetrying]:
|
|
20
|
+
return AsyncRetrying
|
|
21
|
+
|
|
22
|
+
async def call(self, request: Request | dict) -> ApiTypes:
|
|
23
|
+
"""Call any method (with retries) and return `result` from response."""
|
|
24
|
+
return (await self.retry(self._call, request)).result
|
|
25
|
+
|
|
26
|
+
async def _call(self, request: Request | dict) -> Response:
|
|
27
|
+
"""Call any method and return full response."""
|
|
28
|
+
request = Request.model_validate(request)
|
|
29
|
+
self.logger.debug("Sending request: %s", request)
|
|
30
|
+
|
|
31
|
+
response = await self.http.post(
|
|
32
|
+
f"{self.settings.webhook_url}{request.method}",
|
|
33
|
+
headers={"Content-Type": "application/json"},
|
|
34
|
+
json=request.model_dump(mode="json")["parameters"],
|
|
35
|
+
)
|
|
36
|
+
response = self._estimate_call_response(request, response)
|
|
37
|
+
self.logger.debug("Received response: %s", response)
|
|
38
|
+
|
|
39
|
+
return response
|
|
40
|
+
|
|
41
|
+
async def batch(
|
|
42
|
+
self,
|
|
43
|
+
requests: Iterable[Request | dict | tuple[Request | dict, Any]],
|
|
44
|
+
*,
|
|
45
|
+
batch_size: int | None = None,
|
|
46
|
+
with_payload: bool = False,
|
|
47
|
+
) -> AsyncGenerator[ApiTypes | tuple[ApiTypes, Any]]:
|
|
48
|
+
"""Call unlimited sequence of methods within batches and return `result` from responses."""
|
|
49
|
+
batch_size = batch_size or self.settings.batch_size
|
|
50
|
+
|
|
51
|
+
for batched_requests in batched(requests, batch_size):
|
|
52
|
+
if with_payload:
|
|
53
|
+
batched_requests, batched_payloads = zip(*batched_requests, strict=True)
|
|
54
|
+
else:
|
|
55
|
+
batched_payloads = None
|
|
56
|
+
|
|
57
|
+
batched_responses = await self.retry(self._batch, batched_requests)
|
|
58
|
+
for i, response in enumerate(batched_responses):
|
|
59
|
+
if with_payload:
|
|
60
|
+
yield response.result, batched_payloads[i]
|
|
61
|
+
else:
|
|
62
|
+
yield response.result
|
|
63
|
+
|
|
64
|
+
async def _batch(self, requests: Iterable[Request | dict]) -> list[Response]:
|
|
65
|
+
"""Call limited batch of methods and return full responses."""
|
|
66
|
+
commands, request = self._estimate_batch_requests(requests)
|
|
67
|
+
|
|
68
|
+
result = (await self._call(request)).result
|
|
69
|
+
|
|
70
|
+
return self._estimate_batch_responses(commands, result)
|
|
71
|
+
|
|
72
|
+
async def list_sequential(
|
|
73
|
+
self,
|
|
74
|
+
request: Request | dict,
|
|
75
|
+
*,
|
|
76
|
+
list_size: int | None = None,
|
|
77
|
+
) -> AsyncGenerator[ApiTypes]:
|
|
78
|
+
"""Call `list` method and return full `result`.
|
|
79
|
+
|
|
80
|
+
Slow (sequential tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
|
|
81
|
+
"""
|
|
82
|
+
request = Request.model_validate(request)
|
|
83
|
+
list_size = list_size or self.settings.list_size
|
|
84
|
+
|
|
85
|
+
head_request = request.model_copy(deep=True)
|
|
86
|
+
head_request.parameters["start"] = 0
|
|
87
|
+
|
|
88
|
+
head_response = await self.retry(self._call, head_request)
|
|
89
|
+
for item in self._fix_list_result(head_response.result):
|
|
90
|
+
yield item
|
|
91
|
+
|
|
92
|
+
tail_requests = self._estimate_list_tail_requests(head_request, head_response, list_size=list_size)
|
|
93
|
+
for tail_request in tail_requests:
|
|
94
|
+
tail_response = await self.retry(self._call, tail_request)
|
|
95
|
+
|
|
96
|
+
start = tail_request.parameters["start"]
|
|
97
|
+
if tail_response.next and tail_response.next != start + list_size:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Expecting next list chunk to start at {start + list_size}. Got: {tail_response.next}",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
for item in self._fix_list_result(tail_response.result):
|
|
103
|
+
yield item
|
|
104
|
+
|
|
105
|
+
async def list_batched(
|
|
106
|
+
self,
|
|
107
|
+
request: Request | dict,
|
|
108
|
+
*,
|
|
109
|
+
list_size: int | None = None,
|
|
110
|
+
batch_size: int | None = None,
|
|
111
|
+
) -> AsyncGenerator[ApiTypes]:
|
|
112
|
+
"""Call `list` method and return full `result`.
|
|
113
|
+
|
|
114
|
+
Faster (batched tail) list gathering for methods without `filter` parameter (e.g. `department.get`).
|
|
115
|
+
"""
|
|
116
|
+
request = Request.model_validate(request)
|
|
117
|
+
list_size = list_size or self.settings.list_size
|
|
118
|
+
batch_size = batch_size or self.settings.batch_size
|
|
119
|
+
|
|
120
|
+
head_request = request.model_copy(deep=True)
|
|
121
|
+
head_request.parameters["start"] = 0
|
|
122
|
+
|
|
123
|
+
head_response = await self.retry(self._call, head_request)
|
|
124
|
+
for item in self._fix_list_result(head_response.result):
|
|
125
|
+
yield item
|
|
126
|
+
|
|
127
|
+
tail_requests = self._estimate_list_tail_requests(head_request, head_response, list_size=list_size)
|
|
128
|
+
async for tail_response in self.batch(tail_requests, batch_size=batch_size):
|
|
129
|
+
for item in self._fix_list_result(tail_response):
|
|
130
|
+
yield item
|
|
131
|
+
|
|
132
|
+
async def list_batched_no_count(
|
|
133
|
+
self,
|
|
134
|
+
request: ListRequest | dict,
|
|
135
|
+
*,
|
|
136
|
+
id_key: str = "ID",
|
|
137
|
+
list_size: int | None = None,
|
|
138
|
+
batch_size: int | None = None,
|
|
139
|
+
) -> AsyncGenerator[ApiTypes]:
|
|
140
|
+
"""Call `list` method and return full `result`.
|
|
141
|
+
|
|
142
|
+
Fastest (batched, no count) list gathering for methods with `filter` parameter (e.g. `crm.lead.list`).
|
|
143
|
+
"""
|
|
144
|
+
list_size = list_size or self.settings.list_size
|
|
145
|
+
batch_size = batch_size or self.settings.batch_size
|
|
146
|
+
|
|
147
|
+
head_request, tail_request = self._estimate_list_batched_no_count_boundary_requests(request, id_key)
|
|
148
|
+
|
|
149
|
+
boundary_results = [r async for r in self.batch([head_request, tail_request], batch_size=batch_size)]
|
|
150
|
+
head_result, tail_result = tuple(map(self._fix_list_result, boundary_results))
|
|
151
|
+
for item in head_result:
|
|
152
|
+
yield item
|
|
153
|
+
|
|
154
|
+
body_requests = self._estimate_list_batched_no_count_body_requests(
|
|
155
|
+
head_request,
|
|
156
|
+
head_result,
|
|
157
|
+
tail_result,
|
|
158
|
+
id_key,
|
|
159
|
+
list_size,
|
|
160
|
+
)
|
|
161
|
+
async for body_result in self.batch(body_requests, batch_size=batch_size):
|
|
162
|
+
body_result = self._fix_list_result(body_result)
|
|
163
|
+
for item in body_result:
|
|
164
|
+
yield item
|
|
165
|
+
|
|
166
|
+
get_id = itemgetter(id_key)
|
|
167
|
+
max_head_id = max(map(int, map(get_id, head_result)), default=None)
|
|
168
|
+
for item in reversed(tail_result):
|
|
169
|
+
if int(get_id(item)) > max_head_id:
|
|
170
|
+
yield item
|
|
171
|
+
|
|
172
|
+
async def reference_batched_no_count(
|
|
173
|
+
self,
|
|
174
|
+
request: ListRequest | dict,
|
|
175
|
+
updates: Iterable[dict | tuple[dict, Any]],
|
|
176
|
+
*,
|
|
177
|
+
id_key: str = "ID",
|
|
178
|
+
list_size: int | None = None,
|
|
179
|
+
batch_size: int | None = None,
|
|
180
|
+
with_payload: bool = False,
|
|
181
|
+
) -> AsyncGenerator[ApiTypes | tuple[ApiTypes, Any]]:
|
|
182
|
+
"""Call `list` method with reference `updates` for `filter` and return full `result`.
|
|
183
|
+
|
|
184
|
+
Fastest (batched, no count) list gathering for methods with `filter` parameter and required `reference`
|
|
185
|
+
(e.g. `crm.timeline.comment.list`).
|
|
186
|
+
"""
|
|
187
|
+
list_size = list_size or self.settings.list_size
|
|
188
|
+
batch_size = batch_size or self.settings.batch_size
|
|
189
|
+
|
|
190
|
+
head_requests = []
|
|
191
|
+
tail_requests = iter(
|
|
192
|
+
self._estimate_reference_batched_no_count_tail_requests(
|
|
193
|
+
request,
|
|
194
|
+
updates,
|
|
195
|
+
id_key,
|
|
196
|
+
with_payload,
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
while body_requests := head_requests + list(islice(tail_requests, batch_size - len(head_requests))):
|
|
200
|
+
if with_payload:
|
|
201
|
+
body_requests, body_payloads = zip(*body_requests, strict=True)
|
|
202
|
+
else:
|
|
203
|
+
body_payloads = None
|
|
204
|
+
|
|
205
|
+
head_requests = []
|
|
206
|
+
|
|
207
|
+
i = 0
|
|
208
|
+
async for body_result in self.batch(body_requests, batch_size=batch_size):
|
|
209
|
+
body_request = body_requests[i]
|
|
210
|
+
body_payload = body_payloads[i] if with_payload else None
|
|
211
|
+
head_requests, body_result = self._estimate_reference_batched_no_count_result_next(
|
|
212
|
+
body_request,
|
|
213
|
+
body_result,
|
|
214
|
+
body_payload,
|
|
215
|
+
id_key,
|
|
216
|
+
list_size,
|
|
217
|
+
with_payload,
|
|
218
|
+
)
|
|
219
|
+
for item in body_result:
|
|
220
|
+
yield item
|
|
221
|
+
i += 1
|