Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Checks

on:
pull_request:
push:
branches: [master]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
include:
- python: "3.10"
minimum: true
env:
IPGEO_RUN_LIVE_TESTS: "false"
IPGEO_RUN_LIVE_HARDENING: "false"
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: ${{ matrix.python }}
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: "24.22.1"
- run: npm install --global postman-cli@1.73.0
- name: Create test environment
run: |
python -m venv .venv
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
- run: python -m pip install --upgrade pip
- name: Install dependencies
if: ${{ !matrix.minimum }}
run: python -m pip install -e '.[dev]'
- name: Install minimum dependencies
if: matrix.minimum
run: python -m pip install -c tests/constraints-minimum.txt -e '.[dev]'
- run: python -m pip check
- run: ruff check src tests
- run: mypy src
- run: pyright --pythonpath "$(command -v python)"
- run: pytest --cov=src/ipgeolocation --cov-report=term-missing
- run: python -m build
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"
- name: Create audit environment
run: |
python -m venv .venv-audit
.venv-audit/bin/python -m pip install --upgrade pip
.venv-audit/bin/python -m pip install -e '.[audit]'
- name: Audit scanner dependencies
run: .venv-audit/bin/pip-audit --skip-editable
- name: Audit SDK dependencies
run: >-
.venv-audit/bin/pip-audit --skip-editable
--path ".venv/lib/python${{ matrix.python }}/site-packages"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ build/
dist/
*.egg-info/
.venv/
.venv-audit/
venv/
.claude/
CLAUDE-REVIEW.md
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 3.0.0

- Requires Python 3.10 or newer.
- Updated HTTP, certificate, test and build dependency minimums.
- Added dependency auditing and checks across supported Python versions.

## 2.0.0

- Added the Python SDK for the IPGeolocation API unified endpoints.
Expand Down
21 changes: 20 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

## Requirements

- Python 3.8+
- Python 3.10+
- `pip`
- Node.js 24 and Postman CLI 1.73.0 for local HTTP integration tests

## Setup

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -e .[dev]
npm install --global postman-cli@1.73.0
```

## Run Tests
Expand All @@ -19,6 +21,8 @@ pip install -e .[dev]
pytest
```

HTTP integration tests run both clients against a local Postman simulation.

## Coverage Check

```bash
Expand All @@ -33,6 +37,21 @@ mypy src
pyright
```

## Dependency and Build Checks

```bash
python -m build
```

Dependency audits use Python 3.11+ for the scanner and inspect the SDK's test environment:

```bash
python3.11 -m venv .venv-audit
.venv-audit/bin/python -m pip install -e '.[audit]'
.venv-audit/bin/pip-audit --skip-editable \
--path "$(.venv/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
```

## Live Tests

Live tests are disabled by default and consume API credits.
Expand Down
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include CONTRIBUTING.md
include tests/constraints-minimum.txt
recursive-include postman *.yaml *.js
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Typed Python SDK for the [IPGeolocation.io](https://ipgeolocation.io/) IP Geoloc
- Single lookup (`/v3/ipgeo`) and bulk lookup (`/v3/ipgeo-bulk`)
- Sync client built on `requests` and async client built on `httpx`
- Typed response models plus raw JSON/XML methods
- Python 3.8+
- Python 3.10+

## Table of Contents

Expand Down
7 changes: 7 additions & 0 deletions postman/mocks/ipgeolocation/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 1
name: ipgeolocation
origin: https://api.ipgeolocation.io
interceptRequests: true
scenarios:
- name: default
path: default.js
51 changes: 51 additions & 0 deletions postman/mocks/ipgeolocation/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const http = require('node:http');

http.createServer((req, res) => {
const url = new URL(req.url, 'http://localhost');
const reply = (status, body) => {
res.writeHead(status, {
'Content-Type': 'application/json',
'X-Credits-Charged': '1',
'X-Successful-Records': '1',
});
res.end(JSON.stringify(body));
};
if (!['/v3/ipgeo', '/v3/ipgeo-bulk'].includes(url.pathname)) {
return reply(501, { message: 'Endpoint not modeled' });
}
const bulk = url.pathname === '/v3/ipgeo-bulk';
if (req.method !== (bulk ? 'POST' : 'GET')) {
return reply(400, { message: 'Unexpected HTTP method' });
}
const key = url.searchParams.get('apiKey');
const statuses = { BAD_KEY: 401, FORBIDDEN: 403, RATE_LIMIT: 429, SERVER_ERROR: 503 };
if (statuses[key]) return reply(statuses[key], { message: `Test failure for ${key}` });
if (key === 'BAD_JSON') { res.writeHead(200); return res.end('{'); }
if (key === 'BAD_ENCODING') {
res.writeHead(200, { 'Content-Type': 'application/json; charset=unknown-encoding' });
return res.end(JSON.stringify({ ip: '8.8.8.8', location: { city: '東京' } }));
}
if (key === 'LARGE_RESPONSE') {
return reply(200, { ip: '8.8.8.8', location: { city: '東京'.repeat(2000) } });
}
if (key === 'SLOW_REQUEST') {
return setTimeout(() => reply(200, { ip: '8.8.8.8' }), 300);
}
if (key !== 'TEST_KEY &+?=#%' && !(key === null && req.headers.origin === 'https://app.example.test')) {
return reply(401, { message: 'Invalid test key' });
}
if (bulk && (req.headers['content-type'] !== 'application/json' || !Array.isArray(req.body?.ips))) {
return reply(400, { message: 'Expected JSON ips array' });
}
const result = {
ip: bulk ? req.body.ips[0] : url.searchParams.get('ip'),
location: { city: 'Test City' },
_request: {
method: req.method,
query: Object.fromEntries(url.searchParams),
headers: req.headers,
body: bulk ? req.body : null,
},
};
reply(200, bulk ? [result, { message: 'IP address not found' }] : result);
}).listen(process.env.PORT || 4500);
23 changes: 23 additions & 0 deletions postman/simulations/python-sdk/python-sdk.context.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
purpose:
stands_in_for: The IPGeolocation single and bulk endpoints used by the Python SDK.
unblocks: HTTP transport, encoding, authentication, parsing, timeout and response-size regression tests.
assumptions:
- id: transport-compatibility
status: supported
claim: Both clients preserve request and response behavior with the updated dependencies.
reason: Both clients passed the same real HTTP cases using current and minimum dependency versions; the fixture echoes requests for comparison.
verification: On 2026-10-29, 26 HTTP cases passed with both dependency sets. They cover single and bulk lookups, raw responses, query encoding, headers, metadata, errors, malformed JSON, unknown encoding, timeouts and streamed response limits. Simulation request events confirm that the real transports reached the stand-in.
source: tests/test_http_integration.py; src/ipgeolocation/_transport.py
updated_at: '2026-10-29'
- id: live-provider-behavior
status: unknown
claim: The simulation establishes live API behavior and account permissions.
reason: The official API Hub workspace listed no published code mock, and Orbit discovery returned HTTP 403. The fixture models the existing SDK tests and client request builders.
verification: No live provider request or API credential is used.
source: src/ipgeolocation/client.py; src/ipgeolocation/async_client.py; postman/mocks/ipgeolocation/default.js
updated_at: '2026-10-29'
fingerprints:
- id: python-sdk-security-update
date: '2026-10-29'
commit: 65d06151e88d23f1018b6a509cb24cc4302b8342 plus dependency and integration-test changes.
cli: Postman CLI 1.73.0.
8 changes: 8 additions & 0 deletions postman/simulations/python-sdk/python-sdk.sim.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
simulation: python-sdk
routing: path
mocks:
- name: ipgeolocation
routeKey: ipgeolocation
origin:
- api.ipgeolocation.io
path: ../../mocks/ipgeolocation/config.yaml
36 changes: 21 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[build-system]
requires = ["setuptools>=69", "wheel"]
requires = ["setuptools>=84.1.0", "wheel>=0.48.0"]
build-backend = "setuptools.build_meta"

[project]
name = "ipgeolocationio"
version = "2.0.0"
version = "3.0.0"
description = "Typed Python SDK for the IPGeolocation.io IP location API, with sync and async clients."
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.8"
requires-python = ">=3.10"
authors = [
{ name = "IPGeolocation Team", email = "support@ipgeolocation.io" }
]
Expand All @@ -27,29 +27,39 @@ classifiers = [
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Typing :: Typed",
"Topic :: Internet",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"httpx>=0.28,<1",
"requests>=2.32.4,<3",
"httpx>=0.28.1,<1",
"requests>=2.33.0,<3",
"urllib3>=2.8.0,<3",
"anyio>=4.14.2,<5",
"certifi>=2026.10.23",
]

[project.optional-dependencies]
dev = [
"pytest>=8.0.0,<9",
"pytest>=9.0.3,<10",
"pytest-cov>=7.0.0,<8",
"pyright>=1.1.408,<2",
"ruff>=0.15.6,<0.16",
"mypy>=1.19.1,<2",
"types-requests>=2.32.4,<3",
"build>=1.6.1,<2",
"setuptools>=84.1.0",
]
audit = [
"pip-audit>=2.10.1,<3",
"filelock>=4.2.1",
"setuptools>=84.1.0",
]

[project.urls]
Expand Down Expand Up @@ -82,16 +92,14 @@ show_missing = true
skip_covered = false

[tool.ruff]
target-version = "py38"
target-version = "py310"
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I"]

[tool.mypy]
# mypy 1.19+ no longer supports targeting Python 3.8 directly.
# Pyright covers the real Python 3.8 compatibility check for this SDK.
python_version = "3.9"
python_version = "3.10"
files = ["src"]
check_untyped_defs = true
warn_unused_ignores = true
Expand All @@ -101,7 +109,5 @@ no_implicit_optional = true

[tool.pyright]
include = ["src"]
pythonVersion = "3.8"
pythonVersion = "3.10"
typeCheckingMode = "standard"
venvPath = "."
venv = ".venv"
7 changes: 7 additions & 0 deletions tests/constraints-minimum.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
requests==2.33.0
httpx==0.28.1
urllib3==2.8.0
anyio==4.14.2
certifi==2026.10.23
pytest==9.0.3
setuptools==84.1.0
Loading