diff --git a/.convcommit b/.convcommit new file mode 100644 index 0000000..0eed165 --- /dev/null +++ b/.convcommit @@ -0,0 +1,76 @@ +# convcommit - Conventional Commit message builder +# This file is read by the `convcommit` CLI tool to populate +# the interactive selector menus. +# Commit this file to share the project's commit vocabulary with the team. +# +# FORMAT +# type: — commit type option (e.g. fix, feat, docs) +# scope: — commit scope option +# message: — commit message template +# +# SPECIAL PREFIXES +# ~ — marks the default selection +# _ — enables free-text manual input (press ".") +# [X] — forces key letter X for this entry (e.g. [B]build, [W]wip) +# +# HOW TO USE (interactive) +# Run `convcommit` in a git repo. A menu appears for type, scope, message. +# Press the letter in brackets [A][B]... or [.] for free-text input. +# Stage and push in one shot: +# convcommit -a -p +# +# HOW TO USE (direct flags — scripts, AI agents) +# Bypass the selector entirely with explicit flags: +# convcommit --type fix --scope auth --message "fix null pointer" --push +# convcommit -t feat -s api -m "add endpoint" -a -p +# +# SMART PATTERN — stage specific files and commit in one command +# Use --add instead of nested command substitution. +# Anti-pattern (avoid): +# msg=$(convcommit --type fix --message "fix") && git commit -m "$msg" && git push +# Recommended: +# convcommit --add src/auth.sh --type fix --scope auth --message "fix null pointer" --push +# Stage multiple files: +# convcommit --add src/auth.sh --add tests/auth_test.sh -t test -s auth -m "add tests" -p +# +# HOW TO USE (pipe / non-interactive) +# Pipe selections as lines: one per stage (type, scope, message). +# Use the letter shown in the menu, or "." to trigger free-text input. +# Examples: +# printf "G\n.\nfix null pointer in login\n" | convcommit +# printf "F\n\nadd endpoint\n" | convcommit -a -p +# Capture just the formatted message: +# msg=$(printf "G\n\nfix null pointer\n" | convcommit) +# +# OTHER USEFUL FLAGS +# --reset Regenerate this file with the latest defaults +# --help Show all options +# +# INSTALLATION +# convcommit is a single bash file with no dependencies. +# Install it locally in your project: +# curl -fsSL https://raw.githubusercontent.com/francescobianco/convcommit/refs/heads/main/bin/convcommit \ +# -o bin/convcommit && chmod +x bin/convcommit +# Or system-wide: +# curl -fsSL https://raw.githubusercontent.com/francescobianco/convcommit/refs/heads/main/bin/convcommit \ +# -o /usr/local/bin/convcommit && chmod +x /usr/local/bin/convcommit +type:[B]build +type:~chore +type:[D]docs +type:deps +type:feat +type:fix +type:ci +type:init +type:merge +type:perf +type:refactor +type:revert +type:security +type:style +type:test +type:[W]wip +scope:_ +scope:~ +message:_ +message:~_ diff --git a/openapi/__init__.py b/.github/REPOINFO.txt similarity index 100% rename from openapi/__init__.py rename to .github/REPOINFO.txt diff --git a/.github/assets/repo-header-a3.png b/.github/assets/repo-header-a3.png new file mode 100644 index 0000000..9cd8a8b Binary files /dev/null and b/.github/assets/repo-header-a3.png differ diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..0c0ef56 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,37 @@ +name: Python + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ "3.10", "3.11", "3.12" ] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Install dependencies + run: poetry install --no-interaction + + - name: Test + run: poetry run pytest + + - name: Build + run: poetry build diff --git a/.gitignore b/.gitignore index 4f5d603..5f6a183 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# IDEs and editors +.idea/ +.vscode/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -26,6 +30,7 @@ share/python-wheels/ *.egg MANIFEST + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d006704 --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +#!make + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # +# ____ _ # +# / __ \____ ___ ____ ____ _____ (_) ® # +# / / / / __ \/ _ \/ __ \/ __ `/ __ \/ / # +# / /_/ / /_/ / __/ / / / /_/ / /_/ / / # +# \____/ .___/\___/_/ /_/\__,_/ .___/_/ # +# /_/ /_/ # +# # +# The Largest Certified API Marketplace # +# Accelerate Digital Transformation • Simplify Processes • Lead Industry # +# # +# ═══════════════════════════════════════════════════════════════════════ # +# # +# Project: openapi-python-sdk # +# Author: Michael Cuffaro (@maiku1008) # +# Copyright: (c) 2025 Openapi®. All rights reserved. # +# License: MIT # +# Maintainer: Francesco Bianco # +# Contact: https://openapi.com/ # +# Repository: https://github.com/openapi-it/openapi-python-sdk/ # +# Documentation: https://console.openapi.com/ # +# # +# ═══════════════════════════════════════════════════════════════════════ # +# # +# "Truth lies at the source of the stream." # +# — English Proverb # +# # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +## ========= +## Variables +## ========= + +export PATH := $(HOME)/.local/bin:$(PATH) +VERSION := $(shell grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') + +## ==================== +## Development Commands +## ==================== + +dev-push: + @git config credential.helper 'cache --timeout=3600' + @git add . + @git commit -m "$$(read -p 'Commit message: ' msg; echo $$msg)" || true + @git push + +## ================ +## Release Commands +## ================ + +.PHONY: setup build publish release + +setup: + @poetry --version > /dev/null 2>&1 || \ + (echo "Installing Poetry..." && curl -sSL https://install.python-poetry.org | python3 -) + +build: setup + @echo "Building version $(VERSION)..." + @poetry build + +publish: build + @echo "Publishing version $(VERSION) to PyPI..." + @poetry publish + +release: publish + @echo "Tagging release $(VERSION)..." + @git tag -fa "$(VERSION)" -m "Release $(VERSION)" + @git push origin --tags -f + @echo "Released $(VERSION) successfully." \ No newline at end of file diff --git a/README.md b/README.md index 4031a9e..5083a7f 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,80 @@ +
+ + Openapi SDK for Python + -# OpenApi IT Python Client +

Openapi® client for Python

+

The perfect starting point to integrate Openapi® within your Python project

-This client is used to interact with the API found at [openapi.it](https://openapi.it/) +[![Build](https://github.com/openapi/openapi-python-sdk/actions/workflows/python.yml/badge.svg)](https://github.com/openapi/openapi-python-sdk/actions/workflows/python.yml) +[![PyPI Version](https://img.shields.io/pypi/v/openapi-python-sdk)](https://pypi.org/project/openapi-python-sdk/) +[![Python Versions](https://img.shields.io/badge/python-%3E%3D3.10-blue)](https://pypi.org/project/openapi-python-sdk/) +[![License](https://img.shields.io/github/license/openapi/openapi-python-sdk)](LICENSE) +[![Downloads](https://img.shields.io/pypi/dm/openapi-python-sdk)](https://pypi.org/project/openapi-python-sdk/) +
+[![Linux Foundation Member](https://img.shields.io/badge/Linux%20Foundation-Silver%20Member-003778?logo=linux-foundation&logoColor=white)](https://www.linuxfoundation.org/about/members) +
+ +## Overview + +A minimal and agnostic Python SDK for Openapi, inspired by a clean client implementation. This SDK provides only the core HTTP primitives needed to interact with any Openapi service. ## Pre-requisites -Before using the OpenApi IT Python Client, you will need an account at [openapi.it](https://openapi.it/) and an API key to the sandbox and/or production environment +Before using the Openapi Python Client, you will need an account at [Openapi](https://console.openapi.com/) and an API key to the sandbox and/or production environment + +## Features + +- **Agnostic Design**: No API-specific classes, works with any OpenAPI service +- **Minimal Dependencies**: Only requires Python 3.8+ and `requests` +- **OAuth Support**: Built-in OAuth client for token management +- **HTTP Primitives**: GET, POST, PUT, DELETE, PATCH methods +- **Clean Interface**: Similar to the Rust SDK design + +## What you can do + +With the Openapi Python Client, you can easily interact with a variety of services in the Openapi Marketplace. For example, you can: + +- 📩 **Send SMS messages** with delivery reports and custom sender IDs +- 💸 **Process bills and payments** in real time via API +- 🧾 **Send electronic invoices** securely to the Italian Revenue Agency +- 📄 **Generate PDFs** from HTML content, including JavaScript rendering +- ✉️ **Manage certified emails** and legal communications via Italian Legalmail + +For a complete list of all available services, check out the [Openapi Marketplace](https://console.openapi.com/) 🌐 + ## Installation -You can install the OpenApi IT Python Client with the following command using go get: +The package is available on [PyPI](https://pypi.org/project/openapi-python-sdk/) and supports Python 3.10 and above. +Install it with pip: + +```bash +pip install openapi-python-sdk +``` + +If you are using Poetry: ```bash -pip install openapi-cli-python +poetry add openapi-python-sdk ``` - + +No additional configuration is needed. The only runtime dependency is [`httpx`](https://www.python-httpx.org/). + ## Usage +Interaction with the Openapi platform happens in two distinct steps. + +### Step 1 — Generate a token + +Authenticate with your credentials and obtain a short-lived bearer token scoped to the endpoints you need. ```python -from openapi.client import Client, OauthClient +from openapi_python_sdk.client import OauthClient -# Initialize the oauth client on the sandbox environment -oauth_client = OauthClient( - username="", apikey="", test=True -) +oauth = OauthClient(username="", apikey="", test=True) -# Create a token for a list of scopes -resp = oauth_client.create_token( +resp = oauth.create_token( scopes=[ "GET:test.imprese.openapi.it/advance", "POST:test.postontarget.com/fields/country", @@ -36,42 +83,95 @@ resp = oauth_client.create_token( ) token = resp["token"] -# Initialize the client +# Revoke the token when done +oauth.delete_token(id=token) +``` + +### Step 2 — Call an API endpoint + +Use the token to make authenticated requests to any Openapi service. + +```python +from openapi_python_sdk.client import Client + client = Client(token=token) -# Make a request with params +# GET with query params resp = client.request( method="GET", url="https://test.imprese.openapi.it/advance", params={"denominazione": "altravia", "provincia": "RM", "codice_ateco": "6201"}, ) -# Make a request with a payload +# POST with a JSON payload resp = client.request( method="POST", url="https://test.postontarget.com/fields/country", - payload={"limit": 0, "query": { "country_code": "IT"}} + payload={"limit": 0, "query": {"country_code": "IT"}}, ) +``` -# Delete the token -resp = oauth_client.delete_token(id=token) +## Testing + +Install dev dependencies and run the test suite: + +```bash +pip install pytest +pytest ``` +Or with Poetry: + +```bash +poetry install +poetry run pytest +``` + + + + ## Contributing -Contributions are always welcome! +Contributions are always welcome! Whether you want to report bugs, suggest new features, improve documentation, or contribute code, your help is appreciated. + +See [docs/contributing.md](docs/contributing.md) for detailed instructions on how to get started. Please make sure to follow this project's [docs/code-of-conduct.md](docs/code-of-conduct.md) to help maintain a welcoming and collaborative environment. + +## Authors + +Meet the project authors: -See `contributing.md` for ways to get started. +- Michael Cuffaro ([@maiku1008](https://www.github.com/maiku1008)) +- Openapi Team ([@openapi-it](https://github.com/openapi-it)) -Please adhere to this project's `code of conduct`. +## Partners +Meet our partners using Openapi or contributing to this SDK: + +- [Blank](https://www.blank.app/) +- [Credit Safe](https://www.creditsafe.com/) +- [Deliveroo](https://deliveroo.it/) +- [Gruppo MOL](https://molgroupitaly.it/it/) +- [Jakala](https://www.jakala.com/) +- [Octotelematics](https://www.octotelematics.com/) +- [OTOQI](https://otoqi.com/) +- [PWC](https://www.pwc.com/) +- [QOMODO S.R.L.](https://www.qomodo.me/) +- [SOUNDREEF S.P.A.](https://www.soundreef.com/) + +## Our Commitments + +We believe in open source and we act on that belief. We became Silver Members +of the Linux Foundation because we wanted to formally support the ecosystem +we build on every day. Open standards, open collaboration, and open governance +are part of how we work and how we think about software. ## License -[MIT](https://choosealicense.com/licenses/mit/) +This project is licensed under the [MIT License](LICENSE). +The MIT License is a permissive open-source license that allows you to freely use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software, provided that the original copyright notice and this permission notice are included in all copies or substantial portions of the software. -## Authors +In short, you are free to use this SDK in your personal, academic, or commercial projects, with minimal restrictions. The project is provided "as-is", without any warranty of any kind, either expressed or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. + +For more details, see the full license text at the [MIT License page](https://choosealicense.com/licenses/mit/). -- [@maiku1008](https://www.github.com/maiku1008) -- [@openapi-it](https://github.com/openapi-it) diff --git a/docs/code-of-conduct.md b/docs/code-of-conduct.md new file mode 100644 index 0000000..8908ea2 --- /dev/null +++ b/docs/code-of-conduct.md @@ -0,0 +1,30 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project a harassment-free experience for everyone. + +## Our Standards + +Examples of positive behavior: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy toward other community members + +Examples of unacceptable behavior: + +- Harassment, intimidation, or discrimination +- Public or private insults and derogatory comments +- Publishing others’ private information without consent +- Any other conduct reasonably considered inappropriate + +## Enforcement + +Instances of unacceptable behavior may be reported by contacting the project team at ``. All complaints will be reviewed promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..48a76f4 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,45 @@ +# Contributing to OpenApi IT Python SDK + +Thanks for considering contributing! 🎉 +We welcome all kinds of contributions: bug reports, feature requests, documentation improvements, and code enhancements. + +## How to Contribute + +1. **Fork the repository** and clone it locally: + ```bash + git clone https://github.com//.git + ``` + +2. **Create a branch** for your feature or fix: + ```bash + git checkout -b feature/your-feature-name + ``` + +3. **Make your changes** and commit them: + ```bash + git commit -m "Add some feature" + ``` + +4. **Push your branch** to your fork: + ```bash + git push origin feature/your-feature-name + ``` + +5. **Open a Pull Request** describing your changes. + +## Guidelines + +* Follow the existing **Python coding style**. +* Include **tests** for new features or bug fixes when applicable. +* Keep **commit messages clear and concise**. +* Update **documentation** as needed for your changes. + +## Reporting Issues + +To report bugs or request features, please **open an issue** on GitHub including: + +* Clear description of the problem or feature. +* Steps to reproduce (if applicable). +* Relevant logs or screenshots. + +Thank you for helping improve OpenApi IT Python SDK! 🚀 \ No newline at end of file diff --git a/docs/readme-pypi.md b/docs/readme-pypi.md new file mode 100644 index 0000000..60c3790 --- /dev/null +++ b/docs/readme-pypi.md @@ -0,0 +1,102 @@ +# openapi-python-sdk + +A minimal Python SDK for [Openapi®](https://openapi.it) — the largest certified API marketplace in Italy. +Provides the core HTTP primitives to authenticate and interact with any Openapi service, without API-specific coupling. + +## Requirements + +- Python 3.10+ +- An account on [console.openapi.com](https://console.openapi.com/) with a valid API key + +## Installation + +```bash +pip install openapi-python-sdk +``` + +## Usage + +Interaction with the Openapi platform happens in two distinct steps. + +### Step 1 — Generate a token + +```python +from openapi_python_sdk.client import OauthClient + +oauth = OauthClient(username="", apikey="", test=True) + +resp = oauth.create_token( + scopes=[ + "GET:test.imprese.openapi.it/advance", + "POST:test.postontarget.com/fields/country", + ], + ttl=3600, +) +token = resp["token"] + +# Revoke the token when done +oauth.delete_token(id=token) +``` + +### Step 2 — Call an API endpoint + +```python +from openapi_python_sdk.client import Client + +client = Client(token=token) + +# GET with query params +resp = client.request( + method="GET", + url="https://test.imprese.openapi.it/advance", + params={"denominazione": "altravia", "provincia": "RM", "codice_ateco": "6201"}, +) + +# POST with a JSON payload +resp = client.request( + method="POST", + url="https://test.postontarget.com/fields/country", + payload={"limit": 0, "query": {"country_code": "IT"}}, +) +``` + +## Testing + +```bash +pip install pytest +pytest +``` + +## OauthClient API + +| Method | Description | +|---|---| +| `OauthClient(username, apikey, test=False)` | Initialize the OAuth client. Set `test=True` for sandbox. | +| `create_token(scopes, ttl)` | Create a bearer token for the given scopes and TTL (seconds). | +| `get_token(scope)` | Retrieve an existing token by scope. | +| `delete_token(id)` | Revoke a token by ID. | +| `get_scopes(limit=False)` | List available scopes. | +| `get_counters(period, date)` | Retrieve usage counters for a given period and date. | + +## Client API + +| Method | Description | +|---|---| +| `Client(token)` | Initialize the client with a bearer token. | +| `request(method, url, payload, params)` | Execute an HTTP request against any Openapi endpoint. | + +## Links + +- Homepage: [openapi.it](https://openapi.it) +- Console & API keys: [console.openapi.com](https://console.openapi.com/) +- GitHub: [github.com/openapi/openapi-python-sdk](https://github.com/openapi/openapi-python-sdk) +- Issue tracker: [github.com/openapi/openapi-python-sdk/issues](https://github.com/openapi/openapi-python-sdk/issues) + +## License + +MIT — see [LICENSE](https://github.com/openapi/openapi-python-sdk/blob/main/LICENSE) for details. + +## Authors + +- Michael Cuffaro ([@maiku1008](https://github.com/maiku1008)) +- Openapi Team ([@openapi-it](https://github.com/openapi-it)) diff --git a/examples/api_calls.py b/examples/api_calls.py new file mode 100644 index 0000000..990becc --- /dev/null +++ b/examples/api_calls.py @@ -0,0 +1,33 @@ +""" +Step 2 — API Calls +================== +Use a previously generated bearer token to call Openapi endpoints. +Run token_generation.py first, then paste the token below (or pass it via env). + +Usage: + OPENAPI_TOKEN= python examples/api_calls.py +""" + +import os + +from openapi_python_sdk.client import Client + +token = os.environ.get("OPENAPI_TOKEN", "") + +client = Client(token=token) + +# GET request with query params +resp = client.request( + method="GET", + url="https://test.imprese.openapi.it/advance", + params={"denominazione": "altravia", "provincia": "RM", "codice_ateco": "6201"}, +) +print("GET response:", resp) + +# POST request with a JSON payload +resp = client.request( + method="POST", + url="https://test.postontarget.com/fields/country", + payload={"limit": 0, "query": {"country_code": "IT"}}, +) +print("POST response:", resp) diff --git a/examples/token_generation.py b/examples/token_generation.py new file mode 100644 index 0000000..0d29053 --- /dev/null +++ b/examples/token_generation.py @@ -0,0 +1,33 @@ +""" +Step 1 — Token Generation +========================= +Authenticate with your credentials and create a short-lived bearer token +scoped to the API endpoints you need to call. + +Usage: + OPENAPI_USERNAME= OPENAPI_APIKEY= python examples/token_generation.py +""" + +import os + +from openapi_python_sdk.client import OauthClient + +username = os.environ.get("OPENAPI_USERNAME", "") +apikey = os.environ.get("OPENAPI_APIKEY", "") + +oauth = OauthClient(username=username, apikey=apikey, test=True) + +# Create a token valid for 1 hour, scoped to the endpoints you need +resp = oauth.create_token( + scopes=[ + "GET:test.imprese.openapi.it/advance", + "POST:test.postontarget.com/fields/country", + ], + ttl=3600, +) +token = resp["token"] +print(f"Token created: {token}") + +# Revoke the token when you are done +oauth.delete_token(id=token) +print("Token revoked.") diff --git a/openapi_python_sdk/__init__.py b/openapi_python_sdk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openapi/client.py b/openapi_python_sdk/client.py similarity index 100% rename from openapi/client.py rename to openapi_python_sdk/client.py diff --git a/pyproject.toml b/pyproject.toml index 649e3b8..873a0b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,20 @@ [tool.poetry] -name = "openapi-cli-python" -version = "0.1.0" -description = "" +name = "openapi-python-sdk" +version = "0.2.0" +description = "A minimal Python SDK for the Openapi® API marketplace" authors = ["Michael Cuffaro "] -readme = "README.md" -packages = [{include = "openapi_cli_python"}] +readme = "docs/readme-pypi.md" +packages = [{include = "openapi_python_sdk"}] [tool.poetry.dependencies] python = "^3.10" httpx = "^0.24.0" +[tool.poetry.group.dev.dependencies] +pytest = "^8.0" + +[tool.pytest.ini_options] +testpaths = ["tests"] [build-system] requires = ["poetry-core"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..d9ac4dd --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,114 @@ +import unittest +from unittest.mock import MagicMock, patch + +from openapi_python_sdk.client import Client, OauthClient + + +class TestOauthClient(unittest.TestCase): + + @patch("openapi_python_sdk.client.httpx.Client") + def test_create_token(self, mock_httpx): + mock_resp = MagicMock() + mock_resp.json.return_value = {"token": "abc123"} + mock_httpx.return_value.post.return_value = mock_resp + + oauth = OauthClient(username="user", apikey="key", test=True) + resp = oauth.create_token(scopes=["GET:test.example.com/api"], ttl=3600) + + self.assertEqual(resp["token"], "abc123") + mock_httpx.return_value.post.assert_called_once() + + @patch("openapi_python_sdk.client.httpx.Client") + def test_delete_token(self, mock_httpx): + mock_resp = MagicMock() + mock_resp.json.return_value = {"success": True} + mock_httpx.return_value.delete.return_value = mock_resp + + oauth = OauthClient(username="user", apikey="key", test=True) + resp = oauth.delete_token(id="abc123") + + self.assertTrue(resp["success"]) + mock_httpx.return_value.delete.assert_called_once() + + @patch("openapi_python_sdk.client.httpx.Client") + def test_get_scopes(self, mock_httpx): + mock_resp = MagicMock() + mock_resp.json.return_value = {"scopes": ["GET:test.example.com/api"]} + mock_httpx.return_value.get.return_value = mock_resp + + oauth = OauthClient(username="user", apikey="key") + resp = oauth.get_scopes() + + self.assertIn("scopes", resp) + + @patch("openapi_python_sdk.client.httpx.Client") + def test_uses_sandbox_url_when_test_true(self, mock_httpx): + oauth = OauthClient(username="user", apikey="key", test=True) + self.assertIn("test.", oauth.url) + + @patch("openapi_python_sdk.client.httpx.Client") + def test_uses_production_url_by_default(self, mock_httpx): + oauth = OauthClient(username="user", apikey="key") + self.assertNotIn("test.", oauth.url) + + @patch("openapi_python_sdk.client.httpx.Client") + def test_auth_header_is_basic(self, mock_httpx): + oauth = OauthClient(username="user", apikey="key") + self.assertTrue(oauth.auth_header.startswith("Basic ")) + + +class TestClient(unittest.TestCase): + + @patch("openapi_python_sdk.client.httpx.Client") + def test_request_get(self, mock_httpx): + mock_resp = MagicMock() + mock_resp.json.return_value = {"data": []} + mock_httpx.return_value.request.return_value = mock_resp + + client = Client(token="abc123") + resp = client.request( + method="GET", + url="https://test.imprese.openapi.it/advance", + params={"denominazione": "altravia"}, + ) + + self.assertEqual(resp, {"data": []}) + mock_httpx.return_value.request.assert_called_once() + + @patch("openapi_python_sdk.client.httpx.Client") + def test_request_post(self, mock_httpx): + mock_resp = MagicMock() + mock_resp.json.return_value = {"result": "ok"} + mock_httpx.return_value.request.return_value = mock_resp + + client = Client(token="abc123") + resp = client.request( + method="POST", + url="https://test.postontarget.com/fields/country", + payload={"limit": 0, "query": {"country_code": "IT"}}, + ) + + self.assertEqual(resp["result"], "ok") + + @patch("openapi_python_sdk.client.httpx.Client") + def test_auth_header(self, mock_httpx): + client = Client(token="mytoken") + self.assertEqual(client.auth_header, "Bearer mytoken") + self.assertEqual(client.headers["Authorization"], "Bearer mytoken") + + @patch("openapi_python_sdk.client.httpx.Client") + def test_defaults_on_empty_request(self, mock_httpx): + mock_resp = MagicMock() + mock_resp.json.return_value = {} + mock_httpx.return_value.request.return_value = mock_resp + + client = Client(token="tok") + resp = client.request() + + mock_httpx.return_value.request.assert_called_once_with( + method="GET", url="", headers=client.headers, json={}, params={} + ) + + +if __name__ == "__main__": + unittest.main()