diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab76825..e17d92a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: pip install . - name: Run linter - run: git diff --name-only HEAD~10 HEAD | xargs pre-commit run --files + run: pre-commit run --all-files - name: Run tests run: python -m unittest discover src/tests -v @@ -52,7 +52,9 @@ jobs: node-version: '20' - name: Setup semantic-release - run: npm install -g semantic-release @semantic-release/github @semantic-release/changelog @semantic-release/commit-analyzer @semantic-release/git @semantic-release/release-notes-generator semantic-release-pypi + run: | + npm init -y + npm install -g semantic-release @semantic-release/github @semantic-release/commit-analyzer @semantic-release/release-notes-generator - name: Set up Python uses: actions/setup-python@v2 @@ -65,5 +67,5 @@ jobs: - name: Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + # PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} run: npx semantic-release \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7321177 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +default_stages: [ pre-commit ] +fail_fast: true + +repos: + - repo: local + hooks: + - id: black + name: black + entry: black + language: system + types: [ python ] + description: "Black code formatter" + + - id: ruff + name: ruff + description: "Ruff code checker" + entry: ruff check + args: [ --fix, --exit-non-zero-on-fix ] + language: system + types: [ python ] + + + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.2.2 + hooks: + - id: commitizen + stages: [ commit-msg ] \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..48616f1 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "casvisor-python-sdk", + "version": "0.1.0", + "repository": { + "type": "git", + "url": "https://github.com/ljl66-66/casvisor-python-sdk.git" + }, + "release": { + "branches": [ + "master" + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/github", + { + "successComment": false, + "failComment": false, + "failTitle": false, + "releasedLabels": false, + "addReleases": "bottom" + } + ] + ] + } +} \ No newline at end of file diff --git a/setup.py b/setup.py index f57e014..9ca3ced 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -15,4 +15,3 @@ from setuptools import setup setup() - diff --git a/src/casvisor/__init__.py b/src/casvisor/__init__.py index 91c2dfd..39877d2 100644 --- a/src/casvisor/__init__.py +++ b/src/casvisor/__init__.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,7 @@ # limitations under the License. # from .main import CasvisorSDK -from .base import BaseClient,Response +from .base import BaseClient, Response from .record import Record, _RecordSDK -__all__ = ["CasvisorSDK","BaseClient", "Response", "Record", "_RecordSDK"] \ No newline at end of file +__all__ = ["CasvisorSDK", "BaseClient", "Response", "Record", "_RecordSDK"] diff --git a/src/casvisor/base.py b/src/casvisor/base.py index 09bf923..259cdb2 100644 --- a/src/casvisor/base.py +++ b/src/casvisor/base.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -22,13 +22,17 @@ class HttpClient: def do(self, request): pass + class Response: - def __init__(self, status: str, msg: str, data: Union[Dict, List], data2: Union[Dict, List]): + def __init__( + self, status: str, msg: str, data: Union[Dict, List], data2: Union[Dict, List] + ): self.status = status self.msg = msg self.data = data self.data2 = data2 + # Global HTTP client client = requests.Session() @@ -49,7 +53,9 @@ def do_get_response(self, url: str) -> Response: response = json.loads(resp_bytes) if response["status"] != "ok": raise Exception(response["msg"]) - return Response(response["status"], response["msg"], response["data"], response["data2"]) + return Response( + response["status"], response["msg"], response["data"], response["data2"] + ) def do_get_bytes(self, url: str) -> bytes: response = self.do_get_response(url) @@ -62,33 +68,42 @@ def do_get_bytes_raw(self, url: str) -> bytes: raise Exception(response["msg"]) return resp_bytes - def do_post(self, action: str, query_map: Dict[str, str], post_bytes: bytes, is_form: bool, is_file: bool) -> Response: + def do_post( + self, + action: str, + query_map: Dict[str, str], + post_bytes: bytes, + is_form: bool, + is_file: bool, + ) -> Response: url = util.get_url(self.endpoint, action, query_map) content_type, body = self.prepare_body(post_bytes, is_form, is_file) resp_bytes = self.do_post_bytes_raw(url, content_type, body) response = json.loads(resp_bytes) if response["status"] != "ok": raise Exception(response["msg"]) - return Response(response["status"], response["msg"], response["data"], response["data2"]) + return Response( + response["status"], response["msg"], response["data"], response["data2"] + ) def do_post_bytes_raw(self, url: str, content_type: str, body: bytes) -> bytes: if not content_type: content_type = "text/plain;charset=UTF-8" headers = { "Content-Type": content_type, - "Authorization": f"Basic {self.client_id}:{self.client_secret}" + "Authorization": f"Basic {self.client_id}:{self.client_secret}", } resp = client.post(url, headers=headers, data=body) return resp.content def do_get_bytes_raw_without_check(self, url: str) -> bytes: - headers = { - "Authorization": f"Basic {self.client_id}:{self.client_secret}" - } + headers = {"Authorization": f"Basic {self.client_id}:{self.client_secret}"} resp = client.get(url, headers=headers) return resp.content - def prepare_body(self, post_bytes: bytes, is_form: bool, is_file: bool) -> Tuple[str, bytes]: + def prepare_body( + self, post_bytes: bytes, is_form: bool, is_file: bool + ) -> Tuple[str, bytes]: if is_form: if is_file: return util.create_form_file({"file": post_bytes}) diff --git a/src/casvisor/main.py b/src/casvisor/main.py index 7ad6fb3..ffdb344 100644 --- a/src/casvisor/main.py +++ b/src/casvisor/main.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, diff --git a/src/casvisor/record.py b/src/casvisor/record.py index c6a9c5f..8a40e3c 100644 --- a/src/casvisor/record.py +++ b/src/casvisor/record.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -69,22 +69,20 @@ def __init__(self, base_client: BaseClient, organization_name: str): self.organization_name = organization_name def get_records(self) -> List[Record]: - query_map = { - "owner": self.organization_name - } + query_map = {"owner": self.organization_name} url = util.get_url(self.base_client.endpoint, "get-records", query_map) bytes = self.base_client.do_get_bytes(url) return [Record.from_dict(record) for record in json.loads(bytes)] def get_record(self, name: str) -> Record: - query_map = { - "id": f"{self.organization_name}/{name}" - } + query_map = {"id": f"{self.organization_name}/{name}"} url = util.get_url(self.base_client.endpoint, "get-record", query_map) bytes = self.base_client.do_get_bytes(url) return Record.from_dict(json.loads(bytes)) - def get_pagination_records(self, p: int, page_size: int, query_map: Dict[str, str]) -> Tuple[List[Record], int]: + def get_pagination_records( + self, p: int, page_size: int, query_map: Dict[str, str] + ) -> Tuple[List[Record], int]: query_map["owner"] = self.organization_name query_map["p"] = str(p) query_map["page_size"] = str(page_size) @@ -108,14 +106,14 @@ def delete_record(self, record: Record) -> bool: _, affected = self.modify_record("delete-record", record, None) return affected - def modify_record(self, action: str, record: Record, columns: Optional[List[str]]) -> tuple[Dict, bool]: - query_map = { - "id": f"{record.owner}/{record.name}" - } + def modify_record( + self, action: str, record: Record, columns: Optional[List[str]] + ) -> Tuple[Dict, bool]: + query_map = {"id": f"{record.owner}/{record.name}"} if columns: query_map["columns"] = ",".join(columns) if not record.owner: record.owner = "admin" post_bytes = json.dumps(record.to_dict()).encode("utf-8") resp = self.base_client.do_post(action, query_map, post_bytes, False, False) - return resp, resp["data"] == "Affected" \ No newline at end of file + return resp, resp["data"] == "Affected" diff --git a/src/casvisor/util.py b/src/casvisor/util.py index ddc96d1..9857519 100644 --- a/src/casvisor/util.py +++ b/src/casvisor/util.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -13,21 +13,20 @@ # limitations under the License. -from io import BytesIO from requests_toolbelt.multipart.encoder import MultipartEncoder -from typing import Dict +from typing import Dict, Tuple def get_url(base_url: str, action: str, query_map: Dict[str, str]) -> str: query = "&".join([f"{k}={v}" for k, v in query_map.items()]) return f"{base_url}/api/{action}?{query}" -def create_form_file(form_data: Dict[str, bytes]) -> tuple[str, bytes]: - body = BytesIO() + +def create_form_file(form_data: Dict[str, bytes]) -> Tuple[str, bytes]: encoder = MultipartEncoder(fields={k: ("file", v) for k, v in form_data.items()}) return encoder.content_type, encoder.to_string() -def create_form(form_data: Dict[str, str]) -> tuple[str, bytes]: - body = BytesIO() + +def create_form(form_data: Dict[str, str]) -> Tuple[str, bytes]: encoder = MultipartEncoder(fields=form_data) - return encoder.content_type, encoder.to_string() \ No newline at end of file + return encoder.content_type, encoder.to_string() diff --git a/src/tests/__init__.py b/src/tests/__init__.py index 1b49d82..b1ccb70 100644 --- a/src/tests/__init__.py +++ b/src/tests/__init__.py @@ -4,10 +4,10 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and -# limitations under the License. \ No newline at end of file +# limitations under the License. diff --git a/src/tests/test_base.py b/src/tests/test_base.py index f7356d8..08d3cb3 100644 --- a/src/tests/test_base.py +++ b/src/tests/test_base.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -16,6 +16,7 @@ import requests_mock from src.casvisor import BaseClient, Response + class TestBaseClient(unittest.TestCase): def test_base_client_do_get_response_success(self): # Arrange @@ -23,12 +24,7 @@ def test_base_client_do_get_response_success(self): client_secret = "secret456" endpoint = "https://example.com" base_client = BaseClient(client_id, client_secret, endpoint) - mock_response = { - "status": "ok", - "msg": "Success", - "data": [], - "data2": [] - } + mock_response = {"status": "ok", "msg": "Success", "data": [], "data2": []} url = f"{endpoint}/api/action" # Mock HTTP GET response @@ -50,10 +46,7 @@ def test_base_client_do_get_response_error(self): client_secret = "secret456" endpoint = "https://example.com" base_client = BaseClient(client_id, client_secret, endpoint) - mock_response = { - "status": "error", - "msg": "Something went wrong" - } + mock_response = {"status": "error", "msg": "Something went wrong"} url = f"{endpoint}/api/action" # Mock HTTP GET response @@ -74,7 +67,7 @@ def test_do_get_bytes(self): "status": "ok", "msg": "Success", "data": {"key": "value"}, - "data2": [] + "data2": [], } url = f"{endpoint}/api/action" @@ -86,7 +79,7 @@ def test_do_get_bytes(self): # Assert self.assertIsInstance(result, bytes) - self.assertEqual(result.decode('utf-8'), '{"key": "value"}') + self.assertEqual(result.decode("utf-8"), '{"key": "value"}') def test_do_post(self): # Arrange @@ -98,18 +91,14 @@ def test_do_post(self): "status": "ok", "msg": "Success", "data": "Affected", - "data2": [] + "data2": [], } - + # Act with requests_mock.Mocker() as m: m.post(f"{endpoint}/api/action", json=mock_response) response = base_client.do_post( - "action", - {"param": "value"}, - b'{"test": "data"}', - False, - False + "action", {"param": "value"}, b'{"test": "data"}', False, False ) # Assert @@ -121,10 +110,10 @@ def test_prepare_body_form(self): # Arrange client = BaseClient("id", "secret", "endpoint") post_bytes = b'{"field": "value"}' - + # Act content_type, body = client.prepare_body(post_bytes, True, False) - + # Assert - self.assertTrue(content_type.startswith('multipart/form-data')) - self.assertIsInstance(body, bytes) \ No newline at end of file + self.assertTrue(content_type.startswith("multipart/form-data")) + self.assertIsInstance(body, bytes) diff --git a/src/tests/test_main.py b/src/tests/test_main.py index 90b2403..25c5fb6 100644 --- a/src/tests/test_main.py +++ b/src/tests/test_main.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -13,9 +13,9 @@ # limitations under the License. import unittest -from unittest.mock import MagicMock from src.casvisor import BaseClient, CasvisorSDK + class TestCasvisorSDK(unittest.TestCase): def test_casvisor_sdk_initialization(self): # Arrange @@ -25,16 +25,13 @@ def test_casvisor_sdk_initialization(self): organization_name = "org789" application_name = "app012" - # Mock BaseClient - base_client_mock = MagicMock(spec=BaseClient) - # Act sdk = CasvisorSDK( endpoint=endpoint, client_id=client_id, client_secret=client_secret, organization_name=organization_name, - application_name=application_name + application_name=application_name, ) # Assert @@ -44,4 +41,3 @@ def test_casvisor_sdk_initialization(self): self.assertEqual(sdk.organization_name, organization_name) self.assertEqual(sdk.application_name, application_name) self.assertIsInstance(sdk.base_client, BaseClient) - diff --git a/src/tests/test_record.py b/src/tests/test_record.py index 3ace7ea..d9b62c0 100644 --- a/src/tests/test_record.py +++ b/src/tests/test_record.py @@ -4,7 +4,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -16,6 +16,7 @@ from src.casvisor import Record, BaseClient, _RecordSDK import requests_mock + class TestRecord(unittest.TestCase): def test_record_to_dict(self): # Arrange @@ -35,16 +36,16 @@ def test_record_to_dict(self): response="200 OK", provider="provider1", block="block1", - is_triggered=True + is_triggered=True, ) # Act record_dict = record.to_dict() # Assert - self.assertEqual(record_dict['id'], 1) - self.assertEqual(record_dict['owner'], "org123") - self.assertEqual(record_dict['name'], "record_name") + self.assertEqual(record_dict["id"], 1) + self.assertEqual(record_dict["owner"], "org123") + self.assertEqual(record_dict["name"], "record_name") # Additional assertions as needed def test_record_from_dict(self): @@ -65,7 +66,7 @@ def test_record_from_dict(self): "response": "200 OK", "provider": "provider1", "block": "block1", - "is_triggered": True + "is_triggered": True, } # Act @@ -86,7 +87,6 @@ def test_record_sdk_get_records(self): base_client = BaseClient(client_id, client_secret, endpoint) sdk = _RecordSDK(base_client, organization_name) - # 这个测试需要补充更多断言 self.assertIsInstance(sdk, _RecordSDK) def test_get_records(self): @@ -97,37 +97,39 @@ def test_get_records(self): organization_name = "org123" base_client = BaseClient(client_id, client_secret, endpoint) sdk = _RecordSDK(base_client, organization_name) - + mock_response = { "status": "ok", "msg": "Success", - "data": [{ - "id": 1, - "owner": "org123", - "name": "record1", - "created_time": "2023-10-01T12:00:00Z", - "organization": "org123", - "client_ip": "192.168.1.1", - "user": "user1", - "method": "GET", - "request_uri": "/api/endpoint", - "action": "view", - "language": "en", - "object": "object1", - "response": "200 OK", - "provider": "provider1", - "block": "block1", - "is_triggered": True - }], - "data2": [] # 添加缺失的 data2 字段 + "data": [ + { + "id": 1, + "owner": "org123", + "name": "record1", + "created_time": "2023-10-01T12:00:00Z", + "organization": "org123", + "client_ip": "192.168.1.1", + "user": "user1", + "method": "GET", + "request_uri": "/api/endpoint", + "action": "view", + "language": "en", + "object": "object1", + "response": "200 OK", + "provider": "provider1", + "block": "block1", + "is_triggered": True, + } + ], + "data2": [], } - + # Mock HTTP GET response with requests_mock.Mocker() as m: m.get(f"{endpoint}/api/get-records", json=mock_response) # Act records = sdk.get_records() - + # Assert self.assertEqual(len(records), 1) self.assertIsInstance(records[0], Record) @@ -142,38 +144,40 @@ def test_get_pagination_records(self): organization_name = "org123" base_client = BaseClient(client_id, client_secret, endpoint) sdk = _RecordSDK(base_client, organization_name) - + mock_response = { "status": "ok", "msg": "Success", - "data": [{ - "id": 1, - "owner": "org123", - "name": "record1", - "created_time": "2023-10-01T12:00:00Z", - "organization": "org123", - "client_ip": "192.168.1.1", - "user": "user1", - "method": "GET", - "request_uri": "/api/endpoint", - "action": "view", - "language": "en", - "object": "object1", - "response": "200 OK", - "provider": "provider1", - "block": "block1", - "is_triggered": True - }], - "data2": 10 # total count + "data": [ + { + "id": 1, + "owner": "org123", + "name": "record1", + "created_time": "2023-10-01T12:00:00Z", + "organization": "org123", + "client_ip": "192.168.1.1", + "user": "user1", + "method": "GET", + "request_uri": "/api/endpoint", + "action": "view", + "language": "en", + "object": "object1", + "response": "200 OK", + "provider": "provider1", + "block": "block1", + "is_triggered": True, + } + ], + "data2": 10, # total count } - + # Mock HTTP GET response with requests_mock.Mocker() as m: m.get(f"{endpoint}/api/get-records", json=mock_response) # Act records, total = sdk.get_pagination_records(1, 10, {}) - + # Assert self.assertEqual(len(records), 1) self.assertEqual(total, 10) - self.assertIsInstance(records[0], Record) \ No newline at end of file + self.assertIsInstance(records[0], Record)