Skip to content

Commit a0596ef

Browse files
authored
Merge pull request #1745 from stripe/jar/merge-python-beta
Merge to beta
2 parents dd7110d + 60c10c0 commit a0596ef

4 files changed

Lines changed: 110 additions & 2 deletions

File tree

.claude/CLAUDE.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# stripe-python
2+
3+
## Testing
4+
5+
- Run all tests: `just test`
6+
- Run a specific test by name: `just test-one test_name`
7+
- Run a specific test file: `just test tests/path/to/test_file.py`
8+
9+
## Formatting & Linting
10+
11+
- Format: `just format` (uses ruff)
12+
- Lint: `just lint` (uses flake8)
13+
- Typecheck: `just typecheck` (uses pyright)
14+
15+
## Key Locations
16+
17+
- HTTP client (request execution, retries, headers): `stripe/_http_client.py`
18+
- Main client class: `stripe/_stripe_client.py`
19+
- Client options/config: `stripe/_client_options.py`
20+
- API requestor (request building, auth): `stripe/_api_requestor.py`
21+
22+
## Generated Code
23+
24+
- Files containing `File generated from our OpenAPI spec` at the top are generated; do not edit. Similarly, any code block starting with `The beginning of the section generated from our OpenAPI spec` is generated and should not be edited directly.
25+
- If something in a generated file/range needs to be updated, add a summary of the change to your report but don't attempt to edit it directly.
26+
- Most files under `stripe/` resource subdirectories (e.g. `stripe/_customer.py`, `stripe/params/`, `stripe/resources/`) are generated.
27+
- The HTTP client layer (`_http_client.py`, `_stripe_client.py`, `_api_requestor.py`, `_client_options.py`) is NOT generated.
28+
29+
## Conventions
30+
31+
- Uses `requests` library by default for sync HTTP, `httpx` for async
32+
- Type hints throughout
33+
- Virtual env managed in `.venv/`; `just` recipes handle setup automatically
34+
- Work is not complete until `just test`, `just lint` and `just typecheck` complete successfully.
35+
- All code must run on all supported Python versions (full list in the test section of @.github/workflows/ci.yml)
36+
37+
### Comments
38+
39+
- Comments MUST only be used to:
40+
1. Document a function
41+
2. Explain the WHY of a piece of code
42+
3. Explain a particularly complicated piece of code
43+
- Comments NEVER should be used to:
44+
1. Say what used to be there. That's no longer relevant!
45+
2. Explain the WHAT of a piece of code (unless it's very non-obvious)
46+
47+
It's ok not to put comments on/in a function if their addition wouldn't meaningfully clarify anything.

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"venv": true
1515
},
1616
// uses default venv name from Makefile
17-
"python.defaultInterpreterPath": "./venv/bin/python",
17+
"python.defaultInterpreterPath": "",
1818

1919
// Formatting
2020
"editor.formatOnSave": true,

stripe/_api_requestor.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from io import BytesIO, IOBase
22
import json
3+
import os
34
import platform
45
from typing import (
56
Any,
@@ -505,6 +506,23 @@ def specific_oauth_error(self, rbody, rcode, resp, rheaders, error_code):
505506

506507
return None
507508

509+
AI_AGENTS = [
510+
("ANTIGRAVITY_CLI_ALIAS", "antigravity"),
511+
("CLAUDECODE", "claude_code"),
512+
("CLINE_ACTIVE", "cline"),
513+
("CODEX_SANDBOX", "codex_cli"),
514+
("CURSOR_AGENT", "cursor"),
515+
("GEMINI_CLI", "gemini_cli"),
516+
("OPENCODE", "open_code"),
517+
]
518+
519+
@staticmethod
520+
def _detect_ai_agent(environ: Mapping[str, str]) -> str:
521+
for env_var, agent_name in _APIRequestor.AI_AGENTS:
522+
if environ.get(env_var):
523+
return agent_name
524+
return ""
525+
508526
def request_headers(
509527
self, method: HttpVerb, api_mode: ApiMode, options: RequestOptions
510528
):
@@ -515,6 +533,10 @@ def request_headers(
515533
if stripe.app_info:
516534
user_agent += " " + self._format_app_info(stripe.app_info)
517535

536+
agent = self._detect_ai_agent(os.environ)
537+
if agent:
538+
user_agent += " AIAgent/" + agent
539+
518540
ua: Dict[str, Union[str, "AppInfo"]] = {
519541
"bindings_version": VERSION,
520542
"lang": "python",
@@ -533,6 +555,8 @@ def request_headers(
533555
ua[attr] = val
534556
if stripe.app_info:
535557
ua["application"] = stripe.app_info
558+
if agent:
559+
ua["ai_agent"] = agent
536560

537561
headers: Dict[str, str] = {
538562
"X-Stripe-Client-User-Agent": json.dumps(ua),

tests/test_api_requestor.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,8 @@ def test_add_beta_version(self):
695695
== "2024-02-26; feature_beta=v4; another_feature_beta=v2"
696696
)
697697

698-
def test_uses_app_info(self, requestor, http_client_mock):
698+
def test_uses_app_info(self, requestor, mocker, http_client_mock):
699+
mocker.patch.object(_APIRequestor, "_detect_ai_agent", return_value="")
699700
try:
700701
old = stripe.app_info
701702
stripe.set_app_info(
@@ -731,6 +732,42 @@ def test_uses_app_info(self, requestor, http_client_mock):
731732
finally:
732733
stripe.app_info = old
733734

735+
def test_detect_ai_agent(self):
736+
assert (
737+
_APIRequestor._detect_ai_agent({"CLAUDECODE": "1"})
738+
== "claude_code"
739+
)
740+
741+
def test_detect_ai_agent_no_env_vars(self):
742+
assert _APIRequestor._detect_ai_agent({}) == ""
743+
744+
def test_detect_ai_agent_first_match_wins(self):
745+
assert (
746+
_APIRequestor._detect_ai_agent(
747+
{"CURSOR_AGENT": "1", "OPENCODE": "1"}
748+
)
749+
== "cursor"
750+
)
751+
752+
def test_ai_agent_included_in_request_headers(
753+
self, requestor, mocker, http_client_mock
754+
):
755+
mocker.patch.object(
756+
_APIRequestor, "_detect_ai_agent", return_value="cursor"
757+
)
758+
http_client_mock.stub_request(
759+
"get", path=self.v1_path, rbody="{}", rcode=200
760+
)
761+
requestor.request("get", self.v1_path, {}, base_address="api")
762+
763+
last_call = http_client_mock.get_last_call()
764+
ua = last_call.get_raw_header("User-Agent")
765+
assert ua.endswith(" AIAgent/cursor")
766+
client_ua = json.loads(
767+
last_call.get_raw_header("X-Stripe-Client-User-Agent")
768+
)
769+
assert client_ua["ai_agent"] == "cursor"
770+
734771
def test_handles_failed_platform_call(
735772
self, requestor, mocker, http_client_mock
736773
):

0 commit comments

Comments
 (0)