diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..1748b38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,79 @@ +name: 🐞 Bug +description: Report a bug or an issue you've found +title: "[Bug] " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: checkboxes + attributes: + label: Is this a new bug? + description: > + In other words: Is this an error, flaw, failure or fault? Please search issues to see if someone has already reported the bug you encountered. + options: + - label: I believe this is a new bug + required: true + - label: I have searched the existing issues, and I could not find an existing issue for this bug + required: true + + - type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: true + + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true + + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: true + + - type: textarea + attributes: + label: Environment + description: | + examples: + - **OS**: Ubuntu 20.04 + - **Language version**: Python 3.10.11 (`python --version`) + - **SDK Name**: PayPal (If you are using this library as a dependency, please name the parent SDK) + value: | + - **OS**: + - **Language version**: + - **SDK Name**: + render: markdown + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output + description: | + If applicable, log output to help explain your problem. + render: shell + validations: + required: false + + - type: textarea + attributes: + label: Additional Context + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 22a70ea..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug, reported-by-consumer -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. (step 1) -2. (step 2) -3. (step 3) - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Environment** -For example: Windows/Linux - -**Library version** -For example: 0.1.0 - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..c5dd8ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,54 @@ +name: ✨ Feature +description: Propose an extension +title: "[Feature] <title>" +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this feature request! + - type: checkboxes + attributes: + label: Is this your first time submitting a feature request? + description: > + We want to make sure that features are distinct and discoverable, + so that other members of the community can find them and offer their thoughts. + + Issues are the right place to request extensions of existing functionality. + options: + - label: I have searched the existing issues, and I could not find an existing issue for this feature + required: true + - label: I am requesting an extension of the existing functionality + - type: textarea + attributes: + label: Describe the feature + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: Describe alternatives you've considered + description: | + A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: textarea + attributes: + label: Who will this benefit? + description: | + What kind of use case will this feature be useful for? Please be specific and provide examples, this will help us prioritize properly. + validations: + required: false + - type: input + attributes: + label: Are you interested in contributing this feature? + description: Let us know if you want to write some code, and how we can help. + validations: + required: false + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the feature you are suggesting! + validations: + required: false \ No newline at end of file diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 62c2164..06313d4 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ubuntu-22.04] python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 465fbb4..048a1bb 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -15,7 +15,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-22.04] python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..756f41d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing to apimatic/core-lib-python + +Thank you for your interest in contributing! 🎉 Your contributions help make this project better. We value open-source contributions to this library, please take a few minutes to review this guide before you start. + +--- + +## 💡 How to Contribute + +### 📌 1. Reporting Issues + +- Search existing issues before opening a new one. +- Use a **descriptive title** and provide clear steps to reproduce. +- Include relevant logs, screenshots, or error messages. + +### 🔧 2. Making Changes + +#### Step 1: Create a Branch + +- Create a new branch from `main`: + ```sh + git checkout -b your-feature-name + ``` + +#### Step 2: Make Your Changes + +- Follow the project's **coding standards**. +- Add **unit tests** if applicable. +- Ensure your changes **do not break existing functionality**. + +#### Step 3: Commit Changes + +- Use clear and descriptive commit messages: + ```sh + git commit -m "feat: Add feature description" + ``` + +#### Step 4: Push & Open a PR + +- Push your branch: + ```sh + git push origin your-feature-name + ``` +- Open a **Pull Request (PR)** on GitHub: + - Provide a clear **description** of the changes. + - Mention related **issue numbers**. + - Request a **review** from maintainers. + - Make sure your changes clear all the **PR Checks**. + +--- + +## 📜 License + +By contributing, you agree that your contributions will be licensed under the project's [LICENSE](LICENSE). + +--- + +💬 **Questions?** +If you need help, feel free to open issues, we will be happy to help. + +Happy Coding! 🚀 diff --git a/apimatic_core/configurations/global_configuration.py b/apimatic_core/configurations/global_configuration.py index 2befe69..f726b83 100644 --- a/apimatic_core/configurations/global_configuration.py +++ b/apimatic_core/configurations/global_configuration.py @@ -1,3 +1,5 @@ +from requests.structures import CaseInsensitiveDict + from apimatic_core.http.configurations.http_client_configuration import HttpClientConfiguration from apimatic_core.utilities.api_helper import ApiHelper @@ -27,8 +29,8 @@ def __init__( ): self._http_client_configuration = http_client_configuration self._global_errors = {} - self._global_headers = {} - self._additional_headers = {} + self._global_headers = CaseInsensitiveDict() + self._additional_headers = CaseInsensitiveDict() self._auth_managers = {} self._base_uri_executor = None diff --git a/apimatic_core/request_builder.py b/apimatic_core/request_builder.py index 64b6687..d017737 100644 --- a/apimatic_core/request_builder.py +++ b/apimatic_core/request_builder.py @@ -1,3 +1,5 @@ +from requests.structures import CaseInsensitiveDict + from apimatic_core.exceptions.auth_validation_exception import AuthValidationException from apimatic_core.http.request.http_request import HttpRequest from apimatic_core.types.array_serialization_format import SerializationFormats @@ -15,12 +17,11 @@ def get_param_name(param_value): def __init__( self ): - self._server = None self._path = None self._http_method = None self._template_params = {} - self._header_params = {} + self._header_params = CaseInsensitiveDict() self._query_params = {} self._form_params = {} self._additional_form_params = {} @@ -107,10 +108,10 @@ def xml_attributes(self, xml_attributes): def build(self, global_configuration): _url = self.process_url(global_configuration) - _request_headers = self.process_request_headers(global_configuration) - _request_body = self.process_body_params() + _request_headers = self.process_request_headers(global_configuration) + _multipart_params = self.process_multipart_params() http_request = HttpRequest(http_method=self._http_method, @@ -147,14 +148,19 @@ def process_request_headers(self, global_configuration): additional_headers = global_configuration.get_additional_headers() if global_headers: - prepared_headers = {key: str(value) if value is not None else value - for key, value in self._header_params.items()} - request_headers = {**global_headers, **prepared_headers} + request_headers = {**global_headers, **self._header_params} if additional_headers: request_headers.update(additional_headers) - return request_headers + serialized_headers = CaseInsensitiveDict( + { + key: ApiHelper.json_serialize(value) + if value is not None else value + for key, value in request_headers.items() + } + ) + return serialized_headers def process_body_params(self): if self._xml_attributes: diff --git a/setup.py b/setup.py index c620b96..4d51d1e 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name='apimatic-core', - version='0.2.18', + version='0.2.19', description='A library that contains core logic and utilities for ' 'consuming REST APIs using Python SDKs generated by APIMatic.', long_description=long_description, diff --git a/tests/apimatic_core/request_builder_tests/test_request_builder.py b/tests/apimatic_core/request_builder_tests/test_request_builder.py index 7f84b20..ddbd78f 100644 --- a/tests/apimatic_core/request_builder_tests/test_request_builder.py +++ b/tests/apimatic_core/request_builder_tests/test_request_builder.py @@ -176,62 +176,68 @@ def test_additional_query_params(self, input_additional_query_params_value, expe assert http_request.query_url == 'http://localhost:3000/test?{}'.format(expected_additional_query_params_value) @pytest.mark.parametrize('input_local_header_param_value, expected_local_header_param_value', [ - ('string', {'header_param': 'string'}), - (500, {'header_param': 500}), - (500.12, {'header_param': 500.12}), - (str(date(1994, 2, 13)), {'header_param': '1994-02-13'}), + ('string', {'header': 'string'}), + (200, {'header': '200'}), + (200.12, {'header': '200.12'}), + (str(date(1994, 2, 13)), {'header': '1994-02-13'}), (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), - {'header_param': 761117415}), + {'header': '761117415'}), (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), - {'header_param': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + {'header': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), - ([1, 2, 3, 4], {'header_param': [1, 2, 3, 4]}) + {'header': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + ([1, 2, 3, 4], {'header': '[1, 2, 3, 4]'}), + ({'alpha': 'val', 'beta': 'val'}, {'header': '{"alpha": "val", "beta": "val"}'}), + (Base.employee_model(), {'header': ApiHelper.json_serialize(Base.employee_model())}) ]) def test_local_headers(self, input_local_header_param_value, expected_local_header_param_value): http_request = self.new_request_builder \ .header_param(Parameter() - .key('header_param') + .key('header') .value(input_local_header_param_value)) \ .build(self.global_configuration) assert http_request.headers == expected_local_header_param_value @pytest.mark.parametrize('input_global_header_param_value, expected_global_header_param_value', [ - ('my-string', {'header_param': 'my-string'}), - (5000, {'header_param': 5000}), - (5000.12, {'header_param': 5000.12}), - (str(date(1998, 2, 13)), {'header_param': '1998-02-13'}), + ('my-string', {'global_header': 'my-string'}), + (5000, {'global_header': '5000'}), + (5000.12, {'global_header': '5000.12'}), + (str(date(1998, 2, 13)), {'global_header': '1998-02-13'}), (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), - {'header_param': 761117415}), + {'global_header': '761117415'}), (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), - {'header_param': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + {'global_header': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), - ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) + {'global_header': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + ([100, 200, 300, 400], {'global_header': '[100, 200, 300, 400]'}), + ({'key1': 'val1', 'key2': 'val2'}, {'global_header': '{"key1": "val1", "key2": "val2"}'}), + (Base.employee_model(), {'global_header': ApiHelper.json_serialize(Base.employee_model())}) ]) def test_global_headers(self, input_global_header_param_value, expected_global_header_param_value): http_request = self.new_request_builder \ .build(self.global_configuration - .global_header('header_param', input_global_header_param_value)) + .global_header('global_header', input_global_header_param_value)) assert http_request.headers == expected_global_header_param_value @pytest.mark.parametrize('input_additional_header_param_value, expected_additional_header_param_value', [ - ('my-string', {'header_param': 'my-string'}), - (5000, {'header_param': 5000}), - (5000.12, {'header_param': 5000.12}), - (str(date(1998, 2, 13)), {'header_param': '1998-02-13'}), + ('my-string', {'additional_header': 'my-string'}), + (2000, {'additional_header': '2000'}), + (2000.12, {'additional_header': '2000.12'}), + (str(date(1998, 2, 13)), {'additional_header': '1998-02-13'}), (ApiHelper.UnixDateTime.from_datetime(datetime(1994, 2, 13, 5, 30, 15)), - {'header_param': 761117415}), + {'additional_header': '761117415'}), (Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)), - {'header_param': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + {'additional_header': '{}'.format(Base.get_http_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), (Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)), - {'header_param': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), - ([100, 200, 300, 400], {'header_param': [100, 200, 300, 400]}) + {'additional_header': '{}'.format(Base.get_rfc3339_datetime(datetime(1994, 2, 13, 5, 30, 15)))}), + ([100, 200, 300, 400], {'additional_header': '[100, 200, 300, 400]'}), + ({'alpha': 'val1', 'bravo': 'val2'}, {'additional_header': '{"alpha": "val1", "bravo": "val2"}'}), + (Base.employee_model(), {'additional_header': ApiHelper.json_serialize(Base.employee_model())}) ]) def test_additional_headers(self, input_additional_header_param_value, expected_additional_header_param_value): http_request = self.new_request_builder \ .build(self.global_configuration - .additional_header('header_param', input_additional_header_param_value)) + .additional_header('additional_header', input_additional_header_param_value)) assert http_request.headers == expected_additional_header_param_value @pytest.mark.parametrize('input_global_header_param_value,' @@ -510,7 +516,7 @@ def test_file_as_body_param(self, input_body_param_value, expected_body_param_va actual_body_param_value = http_request.parameters assert actual_body_param_value.read() == expected_body_param_value.read() \ - and http_request.headers['content-type'] == expected_content_type + and http_request.headers['Content-Type'] == expected_content_type finally: actual_body_param_value.close() expected_body_param_value.close()