src-py-lib 0.1.4__tar.gz → 0.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/.github/workflows/release.yml +8 -3
  2. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/AGENTS.md +4 -4
  3. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/PKG-INFO +1 -1
  4. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/pyproject.toml +1 -1
  5. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/renovate.json +3 -0
  6. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/clients/github.py +5 -0
  7. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/clients/graphql.py +2 -2
  8. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/clients/sourcegraph.py +31 -3
  9. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/utils/http.py +46 -6
  10. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/utils/logging.py +12 -4
  11. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/tests/test_logging_http_clients.py +70 -6
  12. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/uv.lock +1 -1
  13. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/.github/workflows/ci.yml +0 -0
  14. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/.github/workflows/validate.yml +0 -0
  15. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/.gitignore +0 -0
  16. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/.markdownlint-cli2.yaml +0 -0
  17. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/.python-version +0 -0
  18. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/LICENSE +0 -0
  19. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/README.md +0 -0
  20. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/SECURITY.md +0 -0
  21. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/__init__.py +0 -0
  22. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/clients/__init__.py +0 -0
  23. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/clients/google_sheets.py +0 -0
  24. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/clients/linear.py +0 -0
  25. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/clients/one_password.py +0 -0
  26. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/clients/slack.py +0 -0
  27. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/py.typed +0 -0
  28. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/utils/__init__.py +0 -0
  29. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/utils/config.py +0 -0
  30. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/utils/json_cache.py +0 -0
  31. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/utils/json_types.py +0 -0
  32. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/src/src_py_lib/utils/tsv.py +0 -0
  33. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/tests/test_import.py +0 -0
  34. {src_py_lib-0.1.4 → src_py_lib-0.1.6}/tests/test_tsv.py +0 -0
@@ -12,7 +12,7 @@ on:
12
12
  type: string
13
13
 
14
14
  permissions:
15
- contents: write
15
+ contents: read
16
16
  pull-requests: read
17
17
 
18
18
  concurrency:
@@ -65,8 +65,10 @@ jobs:
65
65
 
66
66
  - name: Validate release inputs
67
67
  id: release
68
+ env:
69
+ RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
68
70
  run: |
69
- release_tag="${{ github.event.inputs.tag || github.ref_name }}"
71
+ release_tag="${RELEASE_TAG}"
70
72
  if [[ ! "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
71
73
  echo "::error title=Invalid release tag::Use a vMAJOR.MINOR.PATCH tag, got '${release_tag}'."
72
74
  exit 1
@@ -214,6 +216,8 @@ jobs:
214
216
  name: Publish GitHub release assets
215
217
  needs: [validate, wheel]
216
218
  runs-on: ubuntu-24.04
219
+ permissions:
220
+ contents: write
217
221
 
218
222
  steps:
219
223
  - name: Download release assets
@@ -226,8 +230,9 @@ jobs:
226
230
  env:
227
231
  GH_TOKEN: ${{ github.token }}
228
232
  GH_REPO: ${{ github.repository }}
233
+ RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
229
234
  run: |
230
- release_tag="${{ github.event.inputs.tag || github.ref_name }}"
235
+ release_tag="${RELEASE_TAG}"
231
236
  notes_path="$(find release-assets -name release-notes.md -print -quit)"
232
237
  mapfile -t release_assets < <(find release-assets -type f ! -name release-notes.md | sort)
233
238
 
@@ -64,7 +64,7 @@ uv run python -m unittest discover -s tests
64
64
  ```sh
65
65
  set -euo pipefail
66
66
 
67
- VERSION=0.1.4
67
+ VERSION=0.1.6
68
68
  BRANCH="release-v${VERSION}"
69
69
 
70
70
  git fetch origin --tags --prune
@@ -116,7 +116,7 @@ rm -rf /tmp/src-py-lib-release-check
116
116
  ```sh
117
117
  set -euo pipefail
118
118
 
119
- VERSION=0.1.4
119
+ VERSION=0.1.6
120
120
  BRANCH="release-v${VERSION}"
121
121
  GH_REPO="sourcegraph/src-py-lib"
122
122
 
@@ -140,7 +140,7 @@ gh pr merge "${BRANCH}" --repo "${GH_REPO}" --squash --delete-branch
140
140
  ```sh
141
141
  set -euo pipefail
142
142
 
143
- VERSION=0.1.4
143
+ VERSION=0.1.6
144
144
 
145
145
  git fetch origin --tags --prune
146
146
  git switch main
@@ -154,7 +154,7 @@ git push origin "v${VERSION}"
154
154
  ```sh
155
155
  set -euo pipefail
156
156
 
157
- VERSION=0.1.4
157
+ VERSION=0.1.6
158
158
  GH_REPO="sourcegraph/src-py-lib"
159
159
 
160
160
  RUN_ID="$(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: src-py-lib
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Reusable libraries for Sourcegraph projects
5
5
  Project-URL: Homepage, https://github.com/sourcegraph/src-py-lib
6
6
  Project-URL: Issues, https://github.com/sourcegraph/src-py-lib/issues
@@ -10,7 +10,7 @@ dev = [
10
10
 
11
11
  [project]
12
12
  name = "src-py-lib"
13
- version = "0.1.4"
13
+ version = "0.1.6"
14
14
  description = "Reusable libraries for Sourcegraph projects"
15
15
  readme = "README.md"
16
16
  requires-python = ">=3.11"
@@ -10,5 +10,8 @@
10
10
  ],
11
11
  "allowedVersions": "<3.12"
12
12
  }
13
+ ],
14
+ "schedule": [
15
+ "at any time"
13
16
  ]
14
17
  }
@@ -113,6 +113,11 @@ def _normalize_github_url(github_url: str) -> str:
113
113
  stripped = github_url.strip().rstrip("/")
114
114
  if "://" not in stripped:
115
115
  stripped = f"https://{stripped}"
116
+ split = urlsplit(stripped)
117
+ if split.scheme != "https":
118
+ raise ValueError(f"GitHub URL must be an https:// URL (got {split.scheme!r})")
119
+ if not split.hostname:
120
+ raise ValueError(f"could not parse hostname from GitHub URL {stripped!r}")
116
121
  return stripped
117
122
 
118
123
 
@@ -9,7 +9,7 @@ from dataclasses import dataclass, field
9
9
  from pathlib import Path
10
10
  from typing import cast
11
11
 
12
- from src_py_lib.utils.http import HTTPClient, HTTPClientError, HTTPResponse
12
+ from src_py_lib.utils.http import HTTPClient, HTTPClientError, HTTPResponse, log_safe_url
13
13
  from src_py_lib.utils.json_types import JSONDict, JSONValue, json_dict, json_list, json_str
14
14
  from src_py_lib.utils.logging import event
15
15
 
@@ -249,7 +249,7 @@ class GraphQLClient:
249
249
  page_number=page_number,
250
250
  page_size=_int_variable(variables, first_variable),
251
251
  cursor_present=variables.get(after_variable) is not None,
252
- url=self.url,
252
+ url=log_safe_url(self.url),
253
253
  variable_names=sorted(variables),
254
254
  query_bytes=len(query.encode("utf-8")),
255
255
  ) as fields:
@@ -182,6 +182,9 @@ class SourcegraphClient:
182
182
  `endpoint` should be the instance base URL, for example
183
183
  `https://sourcegraph.example.com`.
184
184
 
185
+ Plain HTTP endpoints are rejected unless `allow_insecure_http=True` is set
186
+ for local development.
187
+
185
188
  Set `trace=True` to ask Sourcegraph to retain traces for each GraphQL
186
189
  request. Traced requests are available through `drain_traces()` and can be
187
190
  fetched from the instance's Jaeger/debug endpoint with
@@ -192,15 +195,40 @@ class SourcegraphClient:
192
195
  token: str
193
196
  http: HTTPClient = field(default_factory=HTTPClient)
194
197
  trace: bool = False
198
+ allow_insecure_http: bool = False
195
199
  _traces: queue.Queue[SourcegraphTrace] = field(
196
200
  default_factory=lambda: queue.Queue[SourcegraphTrace](), init=False, repr=False
197
201
  )
198
202
 
199
203
  def __post_init__(self) -> None:
200
- self.endpoint = normalize_sourcegraph_endpoint(self.endpoint)
204
+ self.endpoint = normalize_sourcegraph_endpoint(
205
+ self.endpoint,
206
+ require_https=not self.allow_insecure_http,
207
+ )
201
208
 
202
- def graphql(self, query: str, variables: Mapping[str, JSONValue] | None = None) -> JSONDict:
203
- return self._client().execute(query, variables)
209
+ def graphql(
210
+ self,
211
+ query: str,
212
+ variables: Mapping[str, JSONValue] | None = None,
213
+ *,
214
+ follow_pages: bool = True,
215
+ page_size: int | None = None,
216
+ first_variable: str = "first",
217
+ after_variable: str = "after",
218
+ ) -> JSONDict:
219
+ """Execute one Sourcegraph GraphQL operation.
220
+
221
+ Set `follow_pages=False` when the caller owns pagination, such as
222
+ aliased queries with one cursor per alias.
223
+ """
224
+ return self._client().execute(
225
+ query,
226
+ variables,
227
+ follow_pages=follow_pages,
228
+ page_size=page_size,
229
+ first_variable=first_variable,
230
+ after_variable=after_variable,
231
+ )
204
232
 
205
233
  def stream_connection_nodes(
206
234
  self,
@@ -33,6 +33,19 @@ SENSITIVE_HEADER_FRAGMENTS: Final[tuple[str, ...]] = (
33
33
  "secret",
34
34
  "token",
35
35
  )
36
+ SENSITIVE_URL_QUERY_FRAGMENTS: Final[tuple[str, ...]] = (
37
+ "access_token",
38
+ "api-key",
39
+ "api_key",
40
+ "authorization",
41
+ "code",
42
+ "credential",
43
+ "key",
44
+ "password",
45
+ "secret",
46
+ "signature",
47
+ "token",
48
+ )
36
49
 
37
50
  logger = logging.getLogger(__name__)
38
51
 
@@ -150,7 +163,7 @@ class HTTPClient:
150
163
  "http_request",
151
164
  level="debug",
152
165
  method=method,
153
- url=_safe_url(request_url),
166
+ url=log_safe_url(request_url),
154
167
  attempt=attempt,
155
168
  request_headers=_headers_for_log(request_headers),
156
169
  request_bytes=len(body or b""),
@@ -179,7 +192,7 @@ class HTTPClient:
179
192
  if not self._should_retry(response.status_code, attempt):
180
193
  raise HTTPClientError(
181
194
  f"HTTP {response.status_code} for {method} "
182
- f"{_safe_url(request_url)}: {body_text}",
195
+ f"{log_safe_url(request_url)}: {body_text}",
183
196
  status_code=response.status_code,
184
197
  body=body_text,
185
198
  headers=dict(response.headers),
@@ -203,7 +216,7 @@ class HTTPClient:
203
216
  "timed out" if isinstance(exception, httpx.TimeoutException) else "failed"
204
217
  )
205
218
  raise HTTPClientError(
206
- f"HTTP request {failure} for {method} {_safe_url(request_url)}: "
219
+ f"HTTP request {failure} for {method} {log_safe_url(request_url)}: "
207
220
  f"{_exception_message(exception)}"
208
221
  ) from exception
209
222
  record_http_retry()
@@ -246,7 +259,7 @@ class HTTPClient:
246
259
  ), response
247
260
  except json.JSONDecodeError as exception:
248
261
  raise HTTPClientError(
249
- f"Invalid JSON response from {method} {_safe_url(url)}"
262
+ f"Invalid JSON response from {method} {log_safe_url(url)}"
250
263
  ) from exception
251
264
 
252
265
  def _should_retry(self, status_code: int | None, attempt: int) -> bool:
@@ -276,9 +289,31 @@ def _with_query(
276
289
  return f"{url}{separator}{urllib.parse.urlencode(filtered)}"
277
290
 
278
291
 
279
- def _safe_url(url: str) -> str:
292
+ def log_safe_url(url: str) -> str:
293
+ """Return a URL safe to include in logs and exception messages."""
280
294
  split = urllib.parse.urlsplit(url)
281
- return urllib.parse.urlunsplit((split.scheme, split.netloc, split.path, split.query, ""))
295
+ return urllib.parse.urlunsplit(
296
+ (split.scheme, _safe_netloc(split.netloc), split.path, _safe_query(split.query), "")
297
+ )
298
+
299
+
300
+ def _safe_netloc(netloc: str) -> str:
301
+ return netloc.rsplit("@", 1)[-1]
302
+
303
+
304
+ def _safe_query(query: str) -> str:
305
+ if not query:
306
+ return ""
307
+ parts: list[str] = []
308
+ for part in query.split("&"):
309
+ key, separator, _value = part.partition("=")
310
+ if _is_sensitive_query_parameter(urllib.parse.unquote_plus(key)):
311
+ parts.append(f"{key}={REDACTED_HEADER_VALUE}")
312
+ elif separator:
313
+ parts.append(part)
314
+ else:
315
+ parts.append(key)
316
+ return "&".join(parts)
282
317
 
283
318
 
284
319
  def _headers_for_log(headers: Mapping[str, str] | httpx.Headers) -> dict[str, str | list[str]]:
@@ -311,6 +346,11 @@ def _is_sensitive_header(name: str) -> bool:
311
346
  return any(fragment in lowered for fragment in SENSITIVE_HEADER_FRAGMENTS)
312
347
 
313
348
 
349
+ def _is_sensitive_query_parameter(name: str) -> bool:
350
+ lowered = name.lower()
351
+ return any(fragment in lowered for fragment in SENSITIVE_URL_QUERY_FRAGMENTS)
352
+
353
+
314
354
  def _response_http_version(response: httpx.Response) -> str | None:
315
355
  version = response.extensions.get("http_version")
316
356
  if isinstance(version, bytes):
@@ -43,7 +43,9 @@ SRC_LOG_SILENT: Final[str] = "SRC_LOG_SILENT"
43
43
  TRACE_ID_BYTES: Final[int] = 16
44
44
  SPAN_ID_BYTES: Final[int] = 8
45
45
  MEBIBYTE: Final[int] = 1024 * 1024
46
+ REDACTED_LOG_VALUE: Final[str] = "[redacted]"
46
47
  SECRET_FIELD_FRAGMENTS: Final[tuple[str, ...]] = (
48
+ "api-key",
47
49
  "api_key",
48
50
  "authorization",
49
51
  "cookie",
@@ -768,7 +770,7 @@ def sanitized_config_snapshot(config: object) -> dict[str, Any]:
768
770
  if callable(value):
769
771
  continue
770
772
  key_text = str(key)
771
- if any(fragment in key_text.lower() for fragment in SECRET_FIELD_FRAGMENTS):
773
+ if _is_sensitive_log_field(key_text):
772
774
  snapshot[key_text] = _secret_state(value)
773
775
  elif isinstance(value, Path):
774
776
  snapshot[key_text] = str(value)
@@ -937,13 +939,14 @@ def _http_headers(raw_headers: object) -> dict[str, str | list[str]]:
937
939
  if name is None or value is None:
938
940
  continue
939
941
  key = name.lower()
942
+ logged_value = REDACTED_LOG_VALUE if _is_sensitive_log_field(key) else value
940
943
  existing = headers.get(key)
941
944
  if existing is None:
942
- headers[key] = value
945
+ headers[key] = logged_value
943
946
  elif isinstance(existing, list):
944
- existing.append(value)
947
+ existing.append(logged_value)
945
948
  else:
946
- headers[key] = [existing, value]
949
+ headers[key] = [existing, logged_value]
947
950
  return {key: headers[key] for key in sorted(headers)}
948
951
 
949
952
 
@@ -971,6 +974,11 @@ def _is_hex_identifier(value: str, length: int) -> bool:
971
974
  )
972
975
 
973
976
 
977
+ def _is_sensitive_log_field(name: str) -> bool:
978
+ lowered = name.lower()
979
+ return any(fragment in lowered for fragment in SECRET_FIELD_FRAGMENTS)
980
+
981
+
974
982
  def _secret_state(value: object) -> str:
975
983
  if value is None or value == "":
976
984
  return "missing"
@@ -1209,7 +1209,8 @@ class LoggingTest(unittest.TestCase):
1209
1209
  "receive_response_headers.complete "
1210
1210
  "return_value=(b'HTTP/1.1', 200, b'OK', "
1211
1211
  "[(b'Zed', b'last'), (b'Content-Type', b'application/json'), "
1212
- "(b'Alpha', b'first')])"
1212
+ "(b'Set-Cookie', b'session=secret'), "
1213
+ "(b'X-Api-Key', b'secret'), (b'Alpha', b'first')])"
1213
1214
  )
1214
1215
  finally:
1215
1216
  logger = logging.getLogger(logger_name)
@@ -1226,12 +1227,17 @@ class LoggingTest(unittest.TestCase):
1226
1227
  self.assertEqual(response_headers["http_version"], "HTTP/1.1")
1227
1228
  self.assertEqual(response_headers["status_code"], 200)
1228
1229
  self.assertEqual(response_headers["reason_phrase"], "OK")
1229
- self.assertEqual(list(response_headers["headers"]), ["alpha", "content-type", "zed"])
1230
+ self.assertEqual(
1231
+ list(response_headers["headers"]),
1232
+ ["alpha", "content-type", "set-cookie", "x-api-key", "zed"],
1233
+ )
1230
1234
  self.assertEqual(
1231
1235
  response_headers["headers"],
1232
1236
  {
1233
1237
  "alpha": "first",
1234
1238
  "content-type": "application/json",
1239
+ "set-cookie": "[redacted]",
1240
+ "x-api-key": "[redacted]",
1235
1241
  "zed": "last",
1236
1242
  },
1237
1243
  )
@@ -1311,8 +1317,9 @@ class HTTPClientTest(unittest.TestCase):
1311
1317
  client = HTTPClient(max_attempts=1, transport=httpx.MockTransport(handler))
1312
1318
  payload = client.json(
1313
1319
  "POST",
1314
- "https://example.com/api",
1320
+ "https://user:pass@example.com/api?code=oauth",
1315
1321
  headers={"Authorization": "Bearer token"},
1322
+ query={"limit": 10, "access_token": "secret", "signature": "signed"},
1316
1323
  json_body={"hello": "world"},
1317
1324
  )
1318
1325
  finally:
@@ -1332,6 +1339,11 @@ class HTTPClientTest(unittest.TestCase):
1332
1339
  self.assertFalse(any(row.get("logger") in {"httpx", "httpcore"} for row in rows))
1333
1340
  self.assertEqual(http_request["status_code"], 200)
1334
1341
  self.assertEqual(http_request["reason_phrase"], "OK")
1342
+ self.assertEqual(
1343
+ http_request["url"],
1344
+ "https://example.com/api?code=[redacted]&limit=10"
1345
+ "&access_token=[redacted]&signature=[redacted]",
1346
+ )
1335
1347
  self.assertEqual(http_request["request_bytes"], len(b'{"hello": "world"}'))
1336
1348
  self.assertEqual(http_request["request_headers"]["authorization"], "[redacted]")
1337
1349
  self.assertEqual(
@@ -1364,10 +1376,13 @@ class HTTPClientTest(unittest.TestCase):
1364
1376
  )
1365
1377
 
1366
1378
  with self.assertRaisesRegex(HTTPClientError, "rate limited") as raised:
1367
- client.json("GET", "https://example.com/api")
1379
+ client.json("GET", "https://user:pass@example.com/api?access_token=secret")
1368
1380
 
1369
1381
  self.assertEqual(raised.exception.status_code, 429)
1370
1382
  self.assertEqual(raised.exception.body, "rate limited")
1383
+ self.assertIn("https://example.com/api?access_token=[redacted]", str(raised.exception))
1384
+ self.assertNotIn("user:pass", str(raised.exception))
1385
+ self.assertNotIn("secret", str(raised.exception))
1371
1386
 
1372
1387
 
1373
1388
  class ClientTest(unittest.TestCase):
@@ -1409,6 +1424,42 @@ class ClientTest(unittest.TestCase):
1409
1424
  self.assertEqual(http.calls[0]["url"], "https://sourcegraph.example.com/.api/graphql")
1410
1425
  self.assertEqual(http.calls[0]["headers"], {"Authorization": "token token"})
1411
1426
 
1427
+ def test_sourcegraph_client_graphql_can_disable_auto_pagination(self) -> None:
1428
+ http = RecordingHTTP(
1429
+ [
1430
+ {
1431
+ "data": {
1432
+ "users": {
1433
+ "nodes": [{"username": "alice"}],
1434
+ "pageInfo": {"hasNextPage": True, "endCursor": "cursor-1"},
1435
+ }
1436
+ }
1437
+ }
1438
+ ]
1439
+ )
1440
+ client = SourcegraphClient("https://sourcegraph.example.com", "token", http=http)
1441
+ query = """
1442
+ query Users($first: Int!, $after: String) {
1443
+ users(first: $first, after: $after) {
1444
+ nodes { username }
1445
+ pageInfo { hasNextPage endCursor }
1446
+ }
1447
+ }
1448
+ """
1449
+
1450
+ data = client.graphql(query, page_size=1, follow_pages=False)
1451
+
1452
+ self.assertEqual(json_dict(data.get("users"))["nodes"], [{"username": "alice"}])
1453
+ self.assertEqual(len(http.calls), 1)
1454
+ self.assertEqual(http.calls[0]["json_body"]["variables"], {"first": 1})
1455
+
1456
+ def test_sourcegraph_client_rejects_http_endpoint_by_default(self) -> None:
1457
+ with self.assertRaisesRegex(ValueError, "https:// URL"):
1458
+ SourcegraphClient("http://sourcegraph.example.com", "token")
1459
+
1460
+ client = SourcegraphClient("http://localhost:3080", "token", allow_insecure_http=True)
1461
+ self.assertEqual(client.endpoint, "http://localhost:3080")
1462
+
1412
1463
  def test_sourcegraph_client_streams_connection_nodes(self) -> None:
1413
1464
  http = RecordingHTTP(
1414
1465
  [
@@ -1514,7 +1565,7 @@ class ClientTest(unittest.TestCase):
1514
1565
 
1515
1566
  with src.trace_context(root_context):
1516
1567
  self.assertEqual(
1517
- client.graphql("query Viewer { currentUser { username } }"),
1568
+ client.graphql("query Viewer { currentUser { username } }", follow_pages=False),
1518
1569
  {"currentUser": {"username": "alice"}},
1519
1570
  )
1520
1571
  traces = client.drain_traces()
@@ -1780,7 +1831,12 @@ query Items($first: Int!, $after: String, $userId: ID!) {
1780
1831
  },
1781
1832
  ]
1782
1833
  )
1783
- client = GraphQLClient("https://example.com/graphql", {}, "Example", http=http)
1834
+ client = GraphQLClient(
1835
+ "https://user:pass@example.com/graphql?access_token=secret&query=ok",
1836
+ {},
1837
+ "Example",
1838
+ http=http,
1839
+ )
1784
1840
  query = """
1785
1841
  query Items($first: Int!, $after: String, $userId: ID!) {
1786
1842
  viewer { items { nodes { id } pageInfo { hasNextPage endCursor } } }
@@ -1822,6 +1878,10 @@ query Items($first: Int!, $after: String, $userId: ID!) {
1822
1878
  self.assertEqual([row["page_size"] for row in starts], [2, 2])
1823
1879
  self.assertEqual([row["cursor_present"] for row in starts], [False, True])
1824
1880
  self.assertEqual(starts[0]["graphql_client"], "Example")
1881
+ self.assertEqual(
1882
+ starts[0]["url"],
1883
+ "https://example.com/graphql?access_token=[redacted]&query=ok",
1884
+ )
1825
1885
  self.assertEqual(starts[0]["variable_names"], ["after", "first", "userId"])
1826
1886
  self.assertEqual(ends[0]["response_fields"], ["viewer"])
1827
1887
 
@@ -1946,6 +2006,10 @@ query Items($first: Int!, $after: String) {
1946
2006
  graphql_api_url("github.example.com"), "https://github.example.com/api/graphql"
1947
2007
  )
1948
2008
 
2009
+ def test_github_client_rejects_http_enterprise_url(self) -> None:
2010
+ with self.assertRaisesRegex(ValueError, "https:// URL"):
2011
+ graphql_api_url("http://github.example.com")
2012
+
1949
2013
  def test_github_client_validate_queries_viewer(self) -> None:
1950
2014
  http = RecordingHTTP([{"data": {"viewer": {"login": "alice"}}}])
1951
2015
  client = GitHubClient("token", http=http)
@@ -254,7 +254,7 @@ wheels = [
254
254
 
255
255
  [[package]]
256
256
  name = "src-py-lib"
257
- version = "0.1.4"
257
+ version = "0.1.6"
258
258
  source = { editable = "." }
259
259
  dependencies = [
260
260
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes