amochka 0.4.0__tar.gz → 0.4.1__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.
Files changed (27) hide show
  1. {amochka-0.4.0 → amochka-0.4.1}/PKG-INFO +1 -1
  2. {amochka-0.4.0 → amochka-0.4.1}/amochka/__init__.py +1 -1
  3. {amochka-0.4.0 → amochka-0.4.1}/amochka/client.py +6 -0
  4. {amochka-0.4.0 → amochka-0.4.1}/amochka.egg-info/PKG-INFO +1 -1
  5. {amochka-0.4.0 → amochka-0.4.1}/amochka.egg-info/SOURCES.txt +7 -1
  6. {amochka-0.4.0 → amochka-0.4.1}/pyproject.toml +1 -1
  7. amochka-0.4.1/tests/test_cache.py +87 -0
  8. amochka-0.4.1/tests/test_etl.py +34 -0
  9. amochka-0.4.1/tests/test_http.py +168 -0
  10. amochka-0.4.1/tests/test_notes_events.py +30 -0
  11. amochka-0.4.1/tests/test_security.py +179 -0
  12. amochka-0.4.1/tests/test_utils.py +89 -0
  13. {amochka-0.4.0 → amochka-0.4.1}/README.md +0 -0
  14. {amochka-0.4.0 → amochka-0.4.1}/amochka/errors.py +0 -0
  15. {amochka-0.4.0 → amochka-0.4.1}/amochka/etl.py +0 -0
  16. {amochka-0.4.0 → amochka-0.4.1}/amochka.egg-info/dependency_links.txt +0 -0
  17. {amochka-0.4.0 → amochka-0.4.1}/amochka.egg-info/requires.txt +0 -0
  18. {amochka-0.4.0 → amochka-0.4.1}/amochka.egg-info/top_level.txt +0 -0
  19. {amochka-0.4.0 → amochka-0.4.1}/etl/__init__.py +0 -0
  20. {amochka-0.4.0 → amochka-0.4.1}/etl/config.py +0 -0
  21. {amochka-0.4.0 → amochka-0.4.1}/etl/extractors.py +0 -0
  22. {amochka-0.4.0 → amochka-0.4.1}/etl/loaders.py +0 -0
  23. {amochka-0.4.0 → amochka-0.4.1}/etl/migrations/001_create_tables.sql +0 -0
  24. {amochka-0.4.0 → amochka-0.4.1}/etl/run_etl.py +0 -0
  25. {amochka-0.4.0 → amochka-0.4.1}/etl/transformers.py +0 -0
  26. {amochka-0.4.0 → amochka-0.4.1}/setup.cfg +0 -0
  27. {amochka-0.4.0 → amochka-0.4.1}/tests/test_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amochka
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Python library for working with amoCRM API with ETL capabilities
5
5
  Author-email: Timur <timurdt@gmail.com>
6
6
  License: MIT
@@ -2,7 +2,7 @@
2
2
  amochka: Библиотека для работы с API amoCRM.
3
3
  """
4
4
 
5
- __version__ = "0.4.0"
5
+ __version__ = "0.4.1"
6
6
 
7
7
  from .client import AmoCRMClient, CacheConfig
8
8
  from .errors import (
@@ -1591,6 +1591,9 @@ class AmoCRMClient:
1591
1591
  notes = []
1592
1592
  while True:
1593
1593
  response = self._make_request("GET", endpoint, params=params)
1594
+ if not response:
1595
+ self.logger.warning(f"Empty response for notes {entity} {entity_id}, stopping pagination")
1596
+ break
1594
1597
  if response and "_embedded" in response and "notes" in response["_embedded"]:
1595
1598
  notes.extend(response["_embedded"]["notes"])
1596
1599
  if not get_all:
@@ -1670,6 +1673,9 @@ class AmoCRMClient:
1670
1673
  events = []
1671
1674
  while True:
1672
1675
  response = self._make_request("GET", "/api/v4/events", params=params)
1676
+ if not response:
1677
+ self.logger.warning(f"Empty response for events {entity} {entity_id}, stopping pagination")
1678
+ break
1673
1679
  if response and "_embedded" in response and "events" in response["_embedded"]:
1674
1680
  events.extend(response["_embedded"]["events"])
1675
1681
  # Если не нужно получать все страницы, выходим
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amochka
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Python library for working with amoCRM API with ETL capabilities
5
5
  Author-email: Timur <timurdt@gmail.com>
6
6
  License: MIT
@@ -16,4 +16,10 @@ etl/loaders.py
16
16
  etl/run_etl.py
17
17
  etl/transformers.py
18
18
  etl/migrations/001_create_tables.sql
19
- tests/test_client.py
19
+ tests/test_cache.py
20
+ tests/test_client.py
21
+ tests/test_etl.py
22
+ tests/test_http.py
23
+ tests/test_notes_events.py
24
+ tests/test_security.py
25
+ tests/test_utils.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "amochka"
7
- version = "0.4.0"
7
+ version = "0.4.1"
8
8
  description = "Python library for working with amoCRM API with ETL capabilities"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -0,0 +1,87 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import time
5
+
6
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
7
+
8
+ import amochka.client as client_module
9
+ from amochka import AmoCRMClient, CacheConfig
10
+
11
+
12
+ def make_client(cache_config):
13
+ token_data = {"access_token": "token", "expires_at": str(time.time() + 3600)}
14
+ return AmoCRMClient(
15
+ base_url="https://example.amocrm.ru",
16
+ token_file=json.dumps(token_data),
17
+ cache_config=cache_config,
18
+ disable_logging=True,
19
+ )
20
+
21
+
22
+ def make_file_client(tmp_path, lifetime_hours=1):
23
+ token_path = tmp_path / "token.json"
24
+ token_path.write_text(json.dumps({"access_token": "token", "expires_at": str(time.time() + 3600)}))
25
+ cache_dir = tmp_path / "cache"
26
+ cache_config = CacheConfig.file_cache(base_dir=str(cache_dir), lifetime_hours=lifetime_hours)
27
+ return AmoCRMClient(
28
+ base_url="https://example.amocrm.ru",
29
+ token_file=str(token_path),
30
+ cache_config=cache_config,
31
+ disable_logging=True,
32
+ )
33
+
34
+
35
+ def test_memory_cache_ttl(monkeypatch):
36
+ client = make_client(CacheConfig.memory_only())
37
+ client._memory_cache_ttl = 10
38
+
39
+ now = {"value": 100.0}
40
+
41
+ def fake_time():
42
+ return now["value"]
43
+
44
+ monkeypatch.setattr(client_module.time, "time", fake_time)
45
+
46
+ calls = {"count": 0}
47
+
48
+ def fetch():
49
+ calls["count"] += 1
50
+ return [calls["count"]]
51
+
52
+ assert client._get_cached_resource("users", fetch) == [1]
53
+ now["value"] = 105.0
54
+ assert client._get_cached_resource("users", fetch) == [1]
55
+ assert calls["count"] == 1
56
+ now["value"] = 120.0
57
+ assert client._get_cached_resource("users", fetch) == [2]
58
+ assert calls["count"] == 2
59
+
60
+
61
+ def test_file_cache_save_and_load(tmp_path):
62
+ client = make_file_client(tmp_path, lifetime_hours=1)
63
+ payload = [{"id": 1}]
64
+ client._save_cache("users", payload)
65
+ loaded = client._load_cache("users")
66
+ assert loaded == payload
67
+
68
+
69
+ def test_file_cache_expired_returns_none(tmp_path):
70
+ client = make_file_client(tmp_path, lifetime_hours=1)
71
+ payload = [{"id": 1}]
72
+ client._save_cache("users", payload)
73
+ cache_path = client._get_cache_file_path("users")
74
+ with open(cache_path, "r", encoding="utf-8") as handle:
75
+ cache_data = json.loads(handle.read())
76
+ cache_data["last_updated"] = time.time() - 7200
77
+ with open(cache_path, "w", encoding="utf-8") as handle:
78
+ json.dump(cache_data, handle)
79
+ assert client._load_cache("users") is None
80
+
81
+
82
+ def test_get_lifetime_default_and_none():
83
+ config = CacheConfig(lifetime_hours={"users": 5})
84
+ assert config.get_lifetime("users") == 5
85
+ assert config.get_lifetime("unknown") == 24
86
+ config = CacheConfig(lifetime_hours=None)
87
+ assert config.get_lifetime("users") is None
@@ -0,0 +1,34 @@
1
+ import json
2
+ import os
3
+ import sys
4
+
5
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
6
+
7
+ from amochka.etl import write_ndjson
8
+
9
+
10
+ def test_write_ndjson_transform_and_on_record(tmp_path):
11
+ records = [{"id": 1, "updated_at": 123}]
12
+ seen = []
13
+
14
+ def transform(record):
15
+ return {"id": record["id"], "flag": True}
16
+
17
+ def on_record(record):
18
+ seen.append(record["id"])
19
+
20
+ output_path = tmp_path / "out.ndjson"
21
+ count = write_ndjson(
22
+ records,
23
+ output_path,
24
+ entity="lead",
25
+ account_id=42,
26
+ transform=transform,
27
+ on_record=on_record,
28
+ )
29
+ assert count == 1
30
+ payload = json.loads(output_path.read_text().strip())
31
+ assert payload["entity"] == "lead"
32
+ assert payload["account_id"] == 42
33
+ assert payload["payload"]["flag"] is True
34
+ assert seen == [1]
@@ -0,0 +1,168 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import time
5
+
6
+ import pytest
7
+
8
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
9
+
10
+ import amochka.client as client_module
11
+ from amochka import AmoCRMClient, CacheConfig, APIError, NotFoundError, RateLimitError
12
+
13
+
14
+ class FakeResponse:
15
+ def __init__(self, status_code, json_data=None, text="", headers=None):
16
+ self.status_code = status_code
17
+ self._json_data = json_data
18
+ self.text = text
19
+ self.headers = headers or {}
20
+
21
+ def json(self):
22
+ return self._json_data
23
+
24
+
25
+ def make_client(max_retries=0):
26
+ token_data = {"access_token": "token", "expires_at": str(time.time() + 3600)}
27
+ return AmoCRMClient(
28
+ base_url="https://example.amocrm.ru",
29
+ token_file=json.dumps(token_data),
30
+ cache_config=CacheConfig.disabled(),
31
+ disable_logging=True,
32
+ max_retries=max_retries,
33
+ )
34
+
35
+
36
+ def test_make_request_success_json(monkeypatch):
37
+ client = make_client()
38
+
39
+ def fake_request(*args, **kwargs):
40
+ return FakeResponse(200, json_data={"ok": True})
41
+
42
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
43
+ assert client._make_request("GET", "/api/v4/leads") == {"ok": True}
44
+
45
+
46
+ def test_make_request_204_returns_none(monkeypatch):
47
+ client = make_client()
48
+
49
+ def fake_request(*args, **kwargs):
50
+ return FakeResponse(204)
51
+
52
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
53
+ assert client._make_request("DELETE", "/api/v4/leads/1") is None
54
+
55
+
56
+ def test_make_request_404_raises(monkeypatch):
57
+ client = make_client()
58
+
59
+ def fake_request(*args, **kwargs):
60
+ return FakeResponse(404, text="not found")
61
+
62
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
63
+ with pytest.raises(NotFoundError):
64
+ client._make_request("GET", "/api/v4/leads/404")
65
+
66
+
67
+ def test_make_request_bad_request_non_sensitive(monkeypatch):
68
+ client = make_client()
69
+
70
+ def fake_request(*args, **kwargs):
71
+ return FakeResponse(400, text="bad request")
72
+
73
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
74
+ with pytest.raises(APIError) as excinfo:
75
+ client._make_request("GET", "/api/v4/leads")
76
+ assert "bad request" in str(excinfo.value)
77
+
78
+
79
+ def test_make_request_bad_request_sensitive(monkeypatch):
80
+ client = make_client()
81
+
82
+ def fake_request(*args, **kwargs):
83
+ return FakeResponse(400, text="secret token")
84
+
85
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
86
+ with pytest.raises(APIError) as excinfo:
87
+ client._make_request("POST", "/oauth2/access_token")
88
+ assert "secret token" not in str(excinfo.value)
89
+
90
+
91
+ def test_rate_limit_error_includes_retry_after(monkeypatch):
92
+ client = make_client(max_retries=0)
93
+
94
+ def fake_request(*args, **kwargs):
95
+ return FakeResponse(429, text="too many", headers={"Retry-After": "3"})
96
+
97
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
98
+ with pytest.raises(RateLimitError) as excinfo:
99
+ client._make_request("GET", "/api/v4/leads")
100
+ assert excinfo.value.retry_after == "3"
101
+ assert "too many" in str(excinfo.value)
102
+
103
+
104
+ def test_rate_limit_sensitive_hides_body(monkeypatch):
105
+ client = make_client(max_retries=0)
106
+
107
+ def fake_request(*args, **kwargs):
108
+ return FakeResponse(429, text="secret", headers={"Retry-After": "1"})
109
+
110
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
111
+ with pytest.raises(RateLimitError) as excinfo:
112
+ client._make_request("GET", "/oauth2/access_token")
113
+ assert "secret" not in str(excinfo.value)
114
+
115
+
116
+ def test_retries_then_succeeds(monkeypatch):
117
+ client = make_client(max_retries=1)
118
+ responses = [
119
+ FakeResponse(500, text="server error"),
120
+ FakeResponse(200, json_data={"ok": True}),
121
+ ]
122
+ calls = []
123
+
124
+ def fake_request(*args, **kwargs):
125
+ calls.append(1)
126
+ return responses.pop(0)
127
+
128
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
129
+ monkeypatch.setattr(client_module.time, "sleep", lambda *_: None)
130
+ assert client._make_request("GET", "/api/v4/leads") == {"ok": True}
131
+ assert len(calls) == 2
132
+
133
+
134
+ def test_401_triggers_refresh(monkeypatch):
135
+ client = make_client(max_retries=1)
136
+ client.refresh_token = "refresh"
137
+ client.client_id = "id"
138
+ client.client_secret = "secret"
139
+ client.redirect_uri = "https://example.amocrm.ru"
140
+
141
+ responses = [
142
+ FakeResponse(401, text="unauthorized"),
143
+ FakeResponse(200, json_data={"ok": True}),
144
+ ]
145
+ refreshed = {"called": 0}
146
+
147
+ def fake_request(*args, **kwargs):
148
+ return responses.pop(0)
149
+
150
+ def fake_refresh():
151
+ refreshed["called"] += 1
152
+ client.token = "new-token"
153
+
154
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
155
+ monkeypatch.setattr(client, "_refresh_access_token", fake_refresh)
156
+ assert client._make_request("GET", "/api/v4/leads") == {"ok": True}
157
+ assert refreshed["called"] == 1
158
+
159
+
160
+ def test_timeout_raises_api_error(monkeypatch):
161
+ client = make_client(max_retries=0)
162
+
163
+ def fake_request(*args, **kwargs):
164
+ raise client_module.requests.exceptions.Timeout("timeout")
165
+
166
+ monkeypatch.setattr(client_module.requests, "request", fake_request)
167
+ with pytest.raises(APIError):
168
+ client._make_request("GET", "/api/v4/leads")
@@ -0,0 +1,30 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import time
5
+
6
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
7
+
8
+ from amochka import AmoCRMClient, CacheConfig
9
+
10
+
11
+ def make_client():
12
+ token_data = {"access_token": "token", "expires_at": str(time.time() + 3600)}
13
+ return AmoCRMClient(
14
+ base_url="https://example.amocrm.ru",
15
+ token_file=json.dumps(token_data),
16
+ cache_config=CacheConfig.disabled(),
17
+ disable_logging=True,
18
+ )
19
+
20
+
21
+ def test_get_entity_notes_handles_none_response(monkeypatch):
22
+ client = make_client()
23
+ monkeypatch.setattr(client, "_make_request", lambda *args, **kwargs: None)
24
+ assert client.get_entity_notes("lead", 1, get_all=True) == []
25
+
26
+
27
+ def test_get_entity_events_handles_none_response(monkeypatch):
28
+ client = make_client()
29
+ monkeypatch.setattr(client, "_make_request", lambda *args, **kwargs: None)
30
+ assert client.get_entity_events("lead", 1, get_all=True) == []
@@ -0,0 +1,179 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import time
5
+
6
+ import pytest
7
+
8
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
9
+
10
+ import amochka.client as client_module
11
+ from amochka import AmoCRMClient, CacheConfig, AuthenticationError
12
+
13
+
14
+ class FakeResponse:
15
+ def __init__(self, status_code, json_data=None, text="", headers=None):
16
+ self.status_code = status_code
17
+ self._json_data = json_data or {}
18
+ self.text = text
19
+ self.headers = headers or {}
20
+
21
+ def json(self):
22
+ return self._json_data
23
+
24
+
25
+ def _token_payload(expires_at):
26
+ return {
27
+ "access_token": "token",
28
+ "refresh_token": "refresh",
29
+ "expires_at": expires_at,
30
+ "client_id": "client-id",
31
+ "client_secret": "client-secret",
32
+ "redirect_uri": "https://example.amocrm.ru",
33
+ }
34
+
35
+
36
+ def test_validate_base_url_accepts_allowed_domains():
37
+ assert AmoCRMClient._validate_base_url("https://example.amocrm.ru/") == "https://example.amocrm.ru"
38
+ assert AmoCRMClient._validate_base_url("https://sub.amocrm.com") == "https://sub.amocrm.com"
39
+ assert AmoCRMClient._validate_base_url("https://demo.kommo.com/") == "https://demo.kommo.com"
40
+
41
+
42
+ def test_validate_base_url_rejects_invalid():
43
+ with pytest.raises(ValueError):
44
+ AmoCRMClient._validate_base_url("http://example.amocrm.ru")
45
+ with pytest.raises(ValueError):
46
+ AmoCRMClient._validate_base_url("https://example.com")
47
+ with pytest.raises(ValueError):
48
+ AmoCRMClient._validate_base_url("https://example.amocrm.ru:8443")
49
+
50
+
51
+ def test_mask_sensitive():
52
+ assert AmoCRMClient._mask_sensitive(None) == "***"
53
+ assert AmoCRMClient._mask_sensitive("abcd") == "***"
54
+ assert AmoCRMClient._mask_sensitive("abcdefghijkl") == "abcd...ijkl"
55
+
56
+
57
+ def test_env_token_overrides_file(monkeypatch, tmp_path):
58
+ token_path = tmp_path / "token.json"
59
+ token_path.write_text(json.dumps(_token_payload(time.time() + 3600)))
60
+
61
+ monkeypatch.delenv("AMOCRM_ACCESS_TOKEN", raising=False)
62
+ monkeypatch.delenv("AMOCRM_EXPIRES_AT", raising=False)
63
+ monkeypatch.setenv("AMOCRM_ACCESS_TOKEN", "env-token")
64
+ monkeypatch.setenv("AMOCRM_EXPIRES_AT", str(time.time() + 3600))
65
+
66
+ client = AmoCRMClient(
67
+ base_url="https://example.amocrm.ru",
68
+ token_file=str(token_path),
69
+ cache_config=CacheConfig.disabled(),
70
+ disable_logging=True,
71
+ )
72
+ assert client.token == "env-token"
73
+
74
+
75
+ def test_load_token_from_json_string():
76
+ payload = _token_payload(time.time() + 3600)
77
+ client = AmoCRMClient(
78
+ base_url="https://example.amocrm.ru",
79
+ token_file=json.dumps(payload),
80
+ cache_config=CacheConfig.disabled(),
81
+ disable_logging=True,
82
+ )
83
+ assert client.token == "token"
84
+
85
+
86
+ def test_invalid_token_string_raises():
87
+ with pytest.raises(AuthenticationError):
88
+ AmoCRMClient(
89
+ base_url="https://example.amocrm.ru",
90
+ token_file="{bad json",
91
+ cache_config=CacheConfig.disabled(),
92
+ disable_logging=True,
93
+ )
94
+
95
+
96
+ def test_refresh_access_token_success(monkeypatch, tmp_path):
97
+ token_path = tmp_path / "token.json"
98
+ token_path.write_text(json.dumps(_token_payload(time.time() - 10)))
99
+
100
+ def fake_post(*args, **kwargs):
101
+ return FakeResponse(
102
+ 200,
103
+ json_data={
104
+ "access_token": "new-token",
105
+ "refresh_token": "new-refresh",
106
+ "expires_in": 3600,
107
+ },
108
+ )
109
+
110
+ monkeypatch.setattr(client_module.requests, "post", fake_post)
111
+
112
+ client = AmoCRMClient(
113
+ base_url="https://example.amocrm.ru",
114
+ token_file=str(token_path),
115
+ cache_config=CacheConfig.disabled(),
116
+ disable_logging=True,
117
+ )
118
+
119
+ assert client.token == "new-token"
120
+ saved = json.loads(token_path.read_text())
121
+ assert saved["access_token"] == "new-token"
122
+
123
+
124
+ def test_refresh_access_token_failure(monkeypatch, tmp_path):
125
+ token_path = tmp_path / "token.json"
126
+ token_path.write_text(json.dumps(_token_payload(time.time() - 10)))
127
+
128
+ def fake_post(*args, **kwargs):
129
+ return FakeResponse(400, text="bad")
130
+
131
+ monkeypatch.setattr(client_module.requests, "post", fake_post)
132
+
133
+ with pytest.raises(AuthenticationError):
134
+ AmoCRMClient(
135
+ base_url="https://example.amocrm.ru",
136
+ token_file=str(token_path),
137
+ cache_config=CacheConfig.disabled(),
138
+ disable_logging=True,
139
+ )
140
+
141
+
142
+ def test_cache_path_validation():
143
+ with pytest.raises(ValueError):
144
+ CacheConfig._validate_path("../secret", "base_dir")
145
+ with pytest.raises(ValueError):
146
+ CacheConfig._validate_path("", "file")
147
+
148
+
149
+ def test_cache_file_path_stays_in_base_dir(tmp_path):
150
+ token_path = tmp_path / "acct..name.json"
151
+ token_path.write_text(json.dumps(_token_payload(time.time() + 3600)))
152
+ cache_dir = tmp_path / "cache"
153
+ cache_config = CacheConfig.file_cache(base_dir=str(cache_dir))
154
+
155
+ client = AmoCRMClient(
156
+ base_url="https://example.amocrm.ru",
157
+ token_file=str(token_path),
158
+ cache_config=cache_config,
159
+ disable_logging=True,
160
+ )
161
+
162
+ cache_path = client._get_cache_file_path("users")
163
+ assert os.path.realpath(cache_path).startswith(os.path.realpath(str(cache_dir)))
164
+ assert ".." not in os.path.basename(cache_path)
165
+
166
+
167
+ def test_extract_account_name_from_token_file(tmp_path):
168
+ token_path = tmp_path / "accounts" / "bneginskogo_eng.json"
169
+ token_path.parent.mkdir(parents=True, exist_ok=True)
170
+ token_path.write_text(json.dumps(_token_payload(time.time() + 3600)))
171
+
172
+ client = AmoCRMClient(
173
+ base_url="https://example.amocrm.ru",
174
+ token_file=str(token_path),
175
+ cache_config=CacheConfig.disabled(),
176
+ disable_logging=True,
177
+ )
178
+
179
+ assert client._extract_account_name() == "eng"
@@ -0,0 +1,89 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ import time
5
+ from datetime import datetime
6
+
7
+ import pytest
8
+
9
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
10
+
11
+ from amochka import AmoCRMClient, CacheConfig
12
+
13
+
14
+ class PaginationClient(AmoCRMClient):
15
+ def __init__(self, responses):
16
+ token_data = {"access_token": "token", "expires_at": str(time.time() + 3600)}
17
+ super().__init__(
18
+ base_url="https://example.amocrm.ru",
19
+ token_file=json.dumps(token_data),
20
+ cache_config=CacheConfig.disabled(),
21
+ disable_logging=True,
22
+ )
23
+ self._responses = list(responses)
24
+ self.calls = 0
25
+
26
+ def _make_request(self, method, endpoint, params=None, data=None, timeout=10):
27
+ self.calls += 1
28
+ return self._responses.pop(0)
29
+
30
+
31
+ def make_client():
32
+ token_data = {"access_token": "token", "expires_at": str(time.time() + 3600)}
33
+ return AmoCRMClient(
34
+ base_url="https://example.amocrm.ru",
35
+ token_file=json.dumps(token_data),
36
+ cache_config=CacheConfig.disabled(),
37
+ disable_logging=True,
38
+ )
39
+
40
+
41
+ def test_to_timestamp():
42
+ client = make_client()
43
+ dt = datetime(2020, 1, 1, 0, 0, 0)
44
+ assert client._to_timestamp(dt) == int(dt.timestamp())
45
+ assert client._to_timestamp(1700000000) == 1700000000
46
+ assert client._to_timestamp(1700000000.5) == 1700000000
47
+ iso = "2020-01-01T00:00:00"
48
+ assert client._to_timestamp(iso) == int(datetime.fromisoformat(iso).timestamp())
49
+ with pytest.raises(ValueError):
50
+ client._to_timestamp("bad-date")
51
+ with pytest.raises(TypeError):
52
+ client._to_timestamp(object())
53
+
54
+
55
+ def test_format_filter_values():
56
+ client = make_client()
57
+ assert client._format_filter_values(None) is None
58
+ assert client._format_filter_values(123) == "123"
59
+ assert client._format_filter_values(["1", 2]) == ["1", "2"]
60
+
61
+
62
+ def test_extract_collection():
63
+ client = make_client()
64
+ response = {"_embedded": {"items": [{"id": 1}]}}
65
+ assert client._extract_collection(response, ("_embedded", "items")) == [{"id": 1}]
66
+ assert client._extract_collection(response, ("_embedded", "missing")) == []
67
+ assert client._extract_collection({"_embedded": {"items": {"id": 1}}}, ("_embedded", "items")) == []
68
+
69
+
70
+ def test_iterate_paginated_links_next():
71
+ responses = [
72
+ {"_embedded": {"items": [{"id": 1}]}, "_links": {"next": {"href": "next"}}},
73
+ {"_embedded": {"items": [{"id": 2}]}, "_links": {}},
74
+ ]
75
+ client = PaginationClient(responses)
76
+ items = list(client._iterate_paginated("/api/v4/items", data_path=("_embedded", "items")))
77
+ assert [item["id"] for item in items] == [1, 2]
78
+ assert client.calls == 2
79
+
80
+
81
+ def test_iterate_paginated_max_pages():
82
+ responses = [
83
+ {"_embedded": {"items": [{"id": 1}]}, "_page_count": 3},
84
+ {"_embedded": {"items": [{"id": 2}]}, "_page_count": 3},
85
+ ]
86
+ client = PaginationClient(responses)
87
+ items = list(client._iterate_paginated("/api/v4/items", data_path=("_embedded", "items"), max_pages=1))
88
+ assert [item["id"] for item in items] == [1]
89
+ assert client.calls == 1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes