bugpipe 3.0.1__py3-none-any.whl

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.
bugpipe/api/README.md ADDED
@@ -0,0 +1,728 @@
1
+ # Google Issue Tracker API reference
2
+
3
+ Bugpipe reads public Google Issue Tracker issues through the JSON API at
4
+ `https://issuetracker.google.com/action`. It sends requests without cookies
5
+ or authentication tokens.
6
+
7
+ This reference records the request formats and response fields found through
8
+ browser traffic and API tests. {doc}`AUDIT.md <audit>` contains further
9
+ observations and marks which were tested.
10
+
11
+ ## Requests and headers
12
+
13
+ The base URL is `https://issuetracker.google.com/action`. Endpoint paths below
14
+ include `/action`.
15
+
16
+ Search, issue fetches, comments, and updates use POST with JSON arrays.
17
+ Components, trackers, hotlists, relationships, and the health check use GET.
18
+
19
+ | Header | Value | POST | Resource GET |
20
+ |--------------|------------------------------------|------|--------------|
21
+ | Content-Type | `application/json` | Yes | Omit |
22
+ | Origin | `https://issuetracker.google.com` | Yes | Yes |
23
+ | Referer | `https://issuetracker.google.com/` | Yes | Yes |
24
+ | User-Agent | Browser or Googlebot user agent | Yes | Yes |
25
+
26
+ Recorded resource GET requests returned HTTP 400 when sent with
27
+ `Content-Type: application/json`. Omit that header for those requests.
28
+
29
+ The Python client provides search, single and batch issue fetches, comments,
30
+ updates, and the health check. The other endpoints below describe direct HTTP
31
+ requests.
32
+
33
+ ## Response prefix
34
+
35
+ JSON responses start with `)]}'` followed by a newline. Strip this prefix
36
+ before parsing the JSON:
37
+
38
+ ```python
39
+ import json
40
+
41
+ clean = raw_text.removeprefix(")]}'\n")
42
+ data = json.loads(s=clean)
43
+ ```
44
+
45
+ The parser also accepts the prefix followed by CRLF or a literal `\n`.
46
+ The health check returns plain text and has no prefix.
47
+
48
+ ## Known trackers
49
+
50
+ The client's `TRACKERS` list contains these 14 trackers. The root component
51
+ IDs below come from the recorded API observations.
52
+
53
+ | Tracker ID | Name | Root Component | Public URL |
54
+ |------------|--------------|----------------|------------------------------------------|
55
+ | `1` | Pigweed | 1194524 | https://issues.pigweed.dev |
56
+ | `27` | Gerrit | 1370273 | https://issues.gerritcodereview.com |
57
+ | `53` | Git | 1320275 | https://git.issues.gerritcodereview.com |
58
+ | `79` | Skia | 1363359 | https://issues.skia.org |
59
+ | `105` | WebRTC | 1363538 | https://issues.webrtc.org |
60
+ | `131` | libyuv | 1363539 | https://libyuv.issues.chromium.org |
61
+ | `157` | Chromium | 1363614 | https://issues.chromium.org |
62
+ | `183` | Fuchsia | 1360843 | https://issues.fuchsia.dev |
63
+ | `235` | ANGLE | 853171 | https://issues.angleproject.org |
64
+ | `261` | AOMedia | 1597128 | https://aomedia.issues.chromium.org |
65
+ | `287` | WebM | 1615215 | https://issues.webmproject.org |
66
+ | `339` | GN | 1636803 | https://gn.issues.chromium.org |
67
+ | `365` | Project Zero | 1638259 | https://project-zero.issues.chromium.org |
68
+ | `391` | OSS Fuzz | 1638179 | https://issues.oss-fuzz.com |
69
+
70
+ Use `Bugpipe(trackers=None)` to search without a tracker filter, or pass names
71
+ or IDs such as `Bugpipe(trackers=["chromium", "fuchsia"])`. For direct tracker
72
+ lookups, pass the numeric ID to `/action/trackers/{id}`.
73
+
74
+ ## Endpoints
75
+
76
+ ### Search issues
77
+
78
+ ```text
79
+ POST /action/issues/list
80
+ ```
81
+
82
+ Request shape:
83
+
84
+ ```text
85
+ [null, null, null, null, null, TRACKER_FILTER, QUERY_PAYLOAD]
86
+ ```
87
+
88
+ | Position | Field | Type | Value |
89
+ |----------|----------------|----------------------------------|----------------------------------------------------------------------------|
90
+ | `[5]` | tracker_filter | `list[str] \| null` | `["157"]` for Chromium, `["157", "183"]` for both, or `null` for no filter |
91
+ | `[6][0]` | query | `str` | Search query, such as `"status:open"` |
92
+ | `[6][1]` | reserved | `null` | Leave as `null`; recorded tests returned HTTP 400 for other values |
93
+ | `[6][2]` | page_size | `int` | 25, 50, 100, or 250 |
94
+ | `[6][3]` | page_token | `str`, omitted on the first page | Token from the previous response |
95
+
96
+ Search all trackers:
97
+
98
+ ```json
99
+ [null, null, null, null, null, null, ["status:open", null, 50]]
100
+ ```
101
+
102
+ Fetch the next page for Chromium:
103
+
104
+ ```json
105
+ [null, null, null, null, null, ["157"], ["status:open", null, 50, "SOME_PAGE_TOKEN"]]
106
+ ```
107
+
108
+ ### Get a single issue
109
+
110
+ ```text
111
+ POST /action/issues/{issue_id}/getIssue
112
+ POST /action/issues/{issue_id}/getIssue?currentTrackerId={tracker_id}
113
+ ```
114
+
115
+ Request shape:
116
+
117
+ ```text
118
+ [ISSUE_ID, DETAIL_LEVEL, FLAG_2]
119
+ ```
120
+
121
+ | Position | Field | Type | Value |
122
+ |----------|--------------|-------|-----------------------------------------------------------------------|
123
+ | `[0]` | issue_id | `int` | Issue ID |
124
+ | `[1]` | detail_level | `int` | Use `2` to include the body, links, and relationship graph |
125
+ | `[2]` | flag_2 | `int` | The client uses `1`; recorded tests with 0 through 10 found no change |
126
+
127
+ The client sends `[issue_id, 2, 1]` without `currentTrackerId`.
128
+ In the recorded tests, detail levels other than `2` left `TOP[37]`, `TOP[40]`,
129
+ and `TOP[43]` as `null`.
130
+
131
+ ### Batch get issues
132
+
133
+ ```text
134
+ POST /action/issues/batch
135
+ ```
136
+
137
+ Request shape:
138
+
139
+ ```text
140
+ ["b.BatchGetIssuesRequest", null, null, [ISSUE_IDS, DETAIL_LEVEL, FLAG_2]]
141
+ ```
142
+
143
+ | Position | Field | Type | Value |
144
+ |----------|-------|------|-------|
145
+ | `[1]`, `[2]` | unused | `null` | The client sends `null` |
146
+ | `[3][0]` | issue_ids | `list[int]` | Issue IDs to fetch |
147
+ | `[3][1]` | detail_level | `int` | Use `2` to include the body, links, and relationship graph |
148
+ | `[3][2]` | flag_2 | `int` | The client uses `2`; recorded tests with 0 through 10 found no change |
149
+
150
+ Match results by issue ID. Their order may differ from the request order.
151
+
152
+ ### List comments
153
+
154
+ ```text
155
+ POST /action/issues/{issue_id}/listComments
156
+ ```
157
+
158
+ Request shape:
159
+
160
+ ```text
161
+ [ISSUE_ID, SORT_ORDER, PAGE_SIZE, PAGE_TOKEN]
162
+ ```
163
+
164
+ | Position | Field | Type | Value |
165
+ |----------|-------|------|-------|
166
+ | `[0]` | issue_id | `int` | Issue ID |
167
+ | `[1]` | sort_order | `str \| null` | `"ASC"` for oldest first, `"DESC"` or `null` for newest first |
168
+ | `[2]` | page_size | `int` | Up to 500 comments |
169
+ | `[3]` | page_token | `str`, omitted on the first page | Token from the previous response |
170
+
171
+ This endpoint returns text comments. Its `total_count` excludes updates that
172
+ only change fields. Use `/updates` to include field changes.
173
+
174
+ The client defaults to `sort_order="ASC"` and `page_size=500`.
175
+ To request the first three comments, oldest first:
176
+
177
+ ```json
178
+ [496840714, "ASC", 3]
179
+ ```
180
+
181
+ ### List issue updates
182
+
183
+ ```text
184
+ POST /action/issues/{issue_id}/updates
185
+ POST /action/issues/{issue_id}/updates?currentTrackerId={tracker_id}
186
+ ```
187
+
188
+ The client sends the short form, which requests updates newest first:
189
+
190
+ ```text
191
+ [ISSUE_ID]
192
+ ```
193
+
194
+ The API also accepts this format, tested in {doc}`AUDIT.md <audit>`:
195
+
196
+ ```text
197
+ [ISSUE_ID, SORT_ORDER, PAGE_SIZE, PAGE_TOKEN, UNKNOWN_FLAG]
198
+ ```
199
+
200
+ | Position | Field | Type | Value |
201
+ |----------|-------|------|-------|
202
+ | `[0]` | issue_id | `int` | Issue ID |
203
+ | `[1]` | sort_order | `str`, optional | `"ASC"` or `"DESC"`; defaults to `"DESC"` |
204
+ | `[2]` | page_size | `int`, optional | Updates per page; 1100 was tested |
205
+ | `[3]` | page_token | `str \| null` | Token from the previous response |
206
+ | `[4]` | unknown_flag | `int`, optional | `2` in captured browser requests; purpose unknown |
207
+
208
+ Updates can contain comments, field changes, and attachments.
209
+ `IssueUpdatesResult.comments` selects comments and reverses the default
210
+ response order to put the oldest first.
211
+
212
+ ### Get a component
213
+
214
+ ```text
215
+ GET /action/components/{id}
216
+ ```
217
+
218
+ The response type is `b.Component`. The recorded response places the component
219
+ array at `data[0][28]`:
220
+
221
+ | Index | Field | Type | Meaning |
222
+ |-------|-------|------|---------|
223
+ | `[1]` | component_id | `int` | Component ID |
224
+ | `[2]` | parent_component_id | `int` | Parent ID |
225
+ | `[3]` | component_name | `str` | Component name |
226
+ | `[6][0]` | breadcrumb_ids | `list[int]` | IDs from the root to this component |
227
+ | `[6][1]` | breadcrumb_names | `list[str]` | Names from the root to this component |
228
+ | `[6][2]` | custom_field_defs | `list` | Custom field definitions |
229
+ | `[19]` | tracker_id | `int` | Tracker ID |
230
+
231
+ ### Batch get components
232
+
233
+ ```text
234
+ GET /action/components?id=X&id=Y&id=Z
235
+ ```
236
+
237
+ Pass each ID as an `id` query parameter. The response type is
238
+ `b.ListComponentsResponse`. Each component has the same inner array at `[28]`
239
+ as the single component response.
240
+
241
+ ### Get a tracker
242
+
243
+ ```text
244
+ GET /action/trackers/{id}
245
+ ```
246
+
247
+ The response type is `b.Tracker`. The recorded response places the tracker
248
+ array at `data[0][9]`:
249
+
250
+ | Index | Field | Type | Meaning |
251
+ |-------|-------|------|---------|
252
+ | `[0]` | tracker_id | `int` | Tracker ID |
253
+ | `[1]` | root_component_id | `int` | Root component ID |
254
+ | `[2]` | name | `str` | Tracker name |
255
+ | `[5]` | branding | `list` | `[logo_url, type_int, code_of_conduct_url]` |
256
+ | `[7]` | internal_url | `str` | Internal URL, such as `https://g-issues.chromium.org` |
257
+ | `[8]` | public_url | `str` | Public URL, such as `https://issues.chromium.org` |
258
+ | `[10]` | slug | `str \| null` | URL name, such as `"fuchsia"` |
259
+
260
+ ### Get a hotlist
261
+
262
+ ```text
263
+ GET /action/hotlists/{id}
264
+ ```
265
+
266
+ The response type is `b.Hotlist`. The recorded response places the hotlist
267
+ array at `data[0][18]`:
268
+
269
+ | Index | Field | Type | Meaning |
270
+ |-------|-------|------|---------|
271
+ | `[0]` | hotlist_id | `int` | Hotlist ID |
272
+ | `[1]` | name | `str` | Hotlist name |
273
+ | `[2]` | description | `str \| null` | Description |
274
+ | `[6]` | created_at | timestamp | Creation time |
275
+ | `[7]` | modified_at | timestamp | Last change |
276
+ | `[8]` | admins | `list` | Admin user arrays |
277
+
278
+ ### Batch get hotlists
279
+
280
+ ```text
281
+ GET /action/hotlists?id=X&id=Y
282
+ ```
283
+
284
+ Pass each ID as an `id` query parameter. The response type is
285
+ `b.ListHotlistsResponse`.
286
+
287
+ ### List issue relationships
288
+
289
+ ```text
290
+ GET /action/issues/{id}/relationships?relationshipType=1
291
+ ```
292
+
293
+ The response type is `b.ListIssueRelationshipsResponse`.
294
+ `relationshipType=1` selects blocking and blocked-by relationships. An empty
295
+ response is `[["b.ListIssueRelationshipsResponse"]]`.
296
+
297
+ `TOP[36]` on an issue lists the issues it blocks. Use the relationships
298
+ endpoint to find the issues that block it.
299
+
300
+ ### Health check
301
+
302
+ ```text
303
+ GET /action/yes
304
+ ```
305
+
306
+ This endpoint returns `yes` as `text/plain`, without a JSON prefix or
307
+ request body. The recorded checks in {doc}`AUDIT.md <audit>` cover 13
308
+ domains.
309
+
310
+ The client exposes this as `echo()`. It strips whitespace from a successful
311
+ response and returns `"no"` for a non-200 response or an HTTP error.
312
+
313
+ ## Response shapes
314
+
315
+ Here, `data` is the parsed JSON response. Names in uppercase describe values
316
+ rather than literal JSON.
317
+
318
+ ### Search response
319
+
320
+ Type: `b.IssueSearchResponse`.
321
+
322
+ ```text
323
+ data[0] = ["b.IssueSearchResponse", ...]
324
+ data[0][6] = [ISSUES, PAGE_TOKEN, TOTAL_COUNT]
325
+ ```
326
+
327
+ | Path | Value |
328
+ |------|-------|
329
+ | `[0][6][0]` | Issue arrays |
330
+ | `[0][6][1]` | Next page token, or `null` on the last page |
331
+ | `[0][6][2]` | Approximate total matching issues |
332
+
333
+ ### Issue detail response
334
+
335
+ Type: `b.IssueFetchResponse`.
336
+
337
+ ```text
338
+ data[0] = ["b.IssueFetchResponse", PAYLOAD]
339
+ ```
340
+
341
+ The issue array is usually at `data[0][1][22]`. The parser searches `PAYLOAD`
342
+ from the end and selects the first list with an integer at `[1]`.
343
+
344
+ ### Batch response
345
+
346
+ Type: `b.BatchGetIssuesResponse`.
347
+
348
+ ```text
349
+ data[0] = ["b.BatchGetIssuesResponse", null, [[ISSUE_1, ISSUE_2, ...]]]
350
+ ```
351
+
352
+ Issue arrays are at `data[0][2][0]`.
353
+
354
+ ### Comments response
355
+
356
+ Type: `b.ListIssueCommentsResponse`.
357
+
358
+ ```text
359
+ data[0] = ["b.ListIssueCommentsResponse", [COMMENTS, PAGE_TOKEN, TOTAL_COUNT]]
360
+ ```
361
+
362
+ | Path | Value |
363
+ |------|-------|
364
+ | `[0][1][0]` | [Comment arrays](#comment-arrays) |
365
+ | `[0][1][1]` | Next page token, such as `"start_index:2"`, or `null` |
366
+ | `[0][1][2]` | Total text comments |
367
+
368
+ ### Updates response
369
+
370
+ Type: `b.ListIssueUpdatesResponse`.
371
+
372
+ ```text
373
+ data[0] = ["b.ListIssueUpdatesResponse", [UPDATES, PAGE_TOKEN, TOTAL_COUNT]]
374
+ ```
375
+
376
+ | Path | Value |
377
+ |------|-------|
378
+ | `[0][1][0]` | [Update entries](#update-entries) |
379
+ | `[0][1][1]` | Next page token, or `null` |
380
+ | `[0][1][2]` | Total updates |
381
+
382
+ ## Issue array
383
+
384
+ `TOP` means one issue array. `details` means `TOP[2]`.
385
+ The reference layout has 48 positions. Recorded search responses also include
386
+ 47-element arrays, so array length alone does not identify an issue.
387
+
388
+ | Index | Field | Type | Meaning |
389
+ |-------|-------|------|---------|
390
+ | `[1]` | issue_id | `int` | Issue ID; exposed as `Issue.id` |
391
+ | `[2]` | details | `list` | [Issue metadata](#details-array) |
392
+ | `[4]` | created_at | timestamp | Creation time |
393
+ | `[5]` | modified_at | timestamp | Last change |
394
+ | `[6]` | verified_at | timestamp or `null` | Verification time |
395
+ | `[9]` | star_count | `int \| null` | The parser maps `null` to 0 |
396
+ | `[10]` | unknown | `int` | Recorded value: `3` |
397
+ | `[11]` | comment_count | `int` | Includes updates that only change fields |
398
+ | `[12]` | revision_token | `str` | [Revision token](#revision-token) |
399
+ | `[13]` | owner | user array | Assigned owner |
400
+ | `[14]` | custom_field_defs | `list` | Field definitions; values are in `details[14]` |
401
+ | `[33]` | custom_field_refs | `list[list[int]]` | Field IDs for the component |
402
+ | `[34]` | last_activity_at | timestamp | Last comment or substantive field change, per recorded tests |
403
+ | `[35]` | modified_at_mirror | timestamp | Last write; recorded values match or closely follow `TOP[5]` |
404
+ | `[36]` | blocking_issue_ids | `list[int]` | Issues this issue blocks |
405
+ | `[37]` | relationship_graph | `list` | Recorded shape: `[[this_issue, [[blocked_issue]]]]` |
406
+ | `[40]` | links | `list` | Links from the body: `[[[url], null, type_int]]` |
407
+ | `[41]` | tracker_id | `int \| null` | Tracker ID |
408
+ | `[43]` | body | `list \| null` | Description entry from detail or batch fetches |
409
+ | `[46]` | views | `list \| null` | `[24h_views, 7d_views, 30d_views]`; the parser maps missing counts to 0 |
410
+ | `[47]` | last_modifier | user array | Last person to change the issue |
411
+
412
+ The parser reads views at `[46]` and the last modifier at `[47]`.
413
+ Recorded search arrays can place the last modifier at `[46]`.
414
+ The raw fields above include fields that the `Issue` model does not expose.
415
+ See [parser.py](https://github.com/rly0nheart/bugpipe/blob/master/src/bugpipe/api/parser.py) for the fields it extracts.
416
+
417
+ ### Body entry
418
+
419
+ `TOP[43]` contains the description entry. It is `null` in recorded search
420
+ responses. Fetch the issue with detail level `2` to request it.
421
+
422
+ | Index | Field | Type | Meaning |
423
+ |-------|-------|------|---------|
424
+ | `[0]` | text | `str` | Description text, which may contain Markdown |
425
+ | `[1]` | unknown | `null` | Recorded value: `null` |
426
+ | `[2]` | author | user array | Description author |
427
+ | `[3]` | timestamp | timestamp | Description timestamp |
428
+ | `[4]` | unknown | `list` | Recorded value: `[]` |
429
+ | `[5]` | issue_id | `int` | Parent issue |
430
+ | `[6]` | sequence | `int` | Recorded value: `1` |
431
+
432
+ The parser extracts `TOP[43][0]` as `Issue.body`.
433
+
434
+ ### Revision token
435
+
436
+ Recorded `TOP[12]` values decode twice from base64 to
437
+ `{issue_id}-{update_rev}-{comment_rev}`. For example:
438
+
439
+ ```python
440
+ import base64
441
+
442
+ encoded = "TlRFek5USXpORFE0TFRJdE1RPT0="
443
+ plain = base64.b64decode(s=base64.b64decode(s=encoded)).decode()
444
+ issue_id, update_rev, comment_rev = plain.split("-")
445
+ # plain == "513523448-2-1"
446
+ ```
447
+
448
+ The recorded observations describe `update_rev` as matching `TOP[11]` and
449
+ `comment_rev` as tracking comment sequence numbers. These values can exceed
450
+ visible counts when comments have been deleted or restricted.
451
+
452
+ In those observations, the token stayed the same across repeated fetches,
453
+ sessions, and detail/batch requests. No captured request sent it back to the
454
+ server. Comparing tokens can detect a revision change. The Python client
455
+ does not expose or send this token.
456
+
457
+ ## Details array
458
+
459
+ The reference layout for `TOP[2]` has 32 positions.
460
+
461
+ | Index | Field | Type | Meaning |
462
+ |-------|-------|------|---------|
463
+ | `[0]` | component_id | `int` | Resolve the name with a component request |
464
+ | `[1]` | issue_type | `int` | [Issue type](#issue-type) value |
465
+ | `[2]` | status | `int` | [Status](#status) value |
466
+ | `[3]` | priority | `int` | 1 means P0; 5 means P4 |
467
+ | `[4]` | severity | `int \| null` | 1 means S0; 5 means S4 |
468
+ | `[5]` | title | `str` | Issue title |
469
+ | `[6]` | reporter | user array | Person who filed the issue |
470
+ | `[7]` | verifier | user array | Person who verified the fix |
471
+ | `[9]` | ccs | `list[user_array]` | CC users |
472
+ | `[13]` | hotlist_ids | `list[int]` | Hotlists containing the issue |
473
+ | `[14]` | custom_field_values | `list` | [Custom field entries](#custom-fields) |
474
+ | `[16]` | found_in | `list[str]` | Affected versions; API field name: `found_in_versions` |
475
+ | `[19]` | in_prod | `bool \| null` | `true` means observed in production; the parser preserves only `true`, otherwise `None` |
476
+ | `[21]` | duplicate_issue_ids | `list[int]` | Issues marked as duplicates of this issue |
477
+ | `[30]` | collaborators | `list[user_array]` | Collaborator users |
478
+ | `[31]` | issue_access_level | `list` | Recorded value: `[1]`; meaning unverified |
479
+
480
+ ## Custom fields
481
+
482
+ Custom field values are stored at `details[14]`. Trackers can define different
483
+ field IDs. Each entry has this shape:
484
+
485
+ ```text
486
+ [field_id, null, null, null, numeric_value, label_values, null, enum_values, null, display_string, ...]
487
+ ```
488
+
489
+ | Index | Field | Type |
490
+ |-------|-------|------|
491
+ | `[0]` | field_id | `int` |
492
+ | `[4]` | numeric_value | `int \| float \| null` |
493
+ | `[5]` | label_values | Nested lists of strings |
494
+ | `[7]` | enum_values | Nested lists of strings |
495
+ | `[9]` | display_string | `str \| null` |
496
+
497
+ The parser takes the first usable value in this order: number at `[4]`,
498
+ labels at `[5]`, enum values at `[7]`, then display text at `[9]`.
499
+ It flattens label and enum lists.
500
+
501
+ ### Chromium field IDs
502
+
503
+ `CUSTOM_FIELD_IDS` maps these 24 IDs for tracker `157`:
504
+
505
+ | Field ID | Name | Type |
506
+ |----------|-------------------------|-------------|
507
+ | 1222907 | component_tags | `list[str]` |
508
+ | 1223031 | chromium_labels | `list[str]` |
509
+ | 1223032 | design_doc | `str` |
510
+ | 1223033 | build_number | `str` |
511
+ | 1223034 | respin | `str` |
512
+ | 1223081 | flaky_test | `str` |
513
+ | 1223083 | notice | `str` |
514
+ | 1223084 | os | `list[str]` |
515
+ | 1223085 | milestone | `list[str]` |
516
+ | 1223086 | release_block | `list[str]` |
517
+ | 1223087 | merge | `list[str]` |
518
+ | 1223088 | security_release | `list[str]` |
519
+ | 1223131 | design_summary | `str` |
520
+ | 1223134 | merge_request | `list[str]` |
521
+ | 1223135 | vrp_reward | `float` |
522
+ | 1223136 | cve | `list[str]` |
523
+ | 1225154 | next_action | `str` |
524
+ | 1225337 | estimated_days | `float` |
525
+ | 1225362 | backlog_rank | `float` |
526
+ | 1253656 | component_ancestor_tags | `list[str]` |
527
+ | 1300460 | irm_link | `str` |
528
+ | 1358989 | fixed_by_code_changes | `list[str]` |
529
+ | 1410892 | cwe_id | `float` |
530
+ | 1544844 | introduced_in | `str` |
531
+
532
+ The parser stores unknown IDs in `Issue.custom_fields` as `field_{id}`.
533
+ It maps `chromium_labels` to `Issue.labels`. Known fields without a dedicated
534
+ `Issue` attribute, including `design_doc`, `design_summary`, `respin`, and
535
+ `backlog_rank`, remain in `custom_fields` under their mapped names.
536
+
537
+ ## User arrays
538
+
539
+ User fields include the reporter, owner, verifier, and CC users. A typical
540
+ array is:
541
+
542
+ ```json
543
+ [null, "user@example.com", 1, ["google_domain"]]
544
+ ```
545
+
546
+ The parser returns the first string containing `@`. It returns `None` when
547
+ there is no such string. It does not decode the other user fields.
548
+
549
+ ## Timestamps
550
+
551
+ Timestamps contain Unix seconds and an optional nanosecond value:
552
+
553
+ ```json
554
+ [1657579144, 285000000]
555
+ ```
556
+
557
+ The parser treats missing nanoseconds as 0 and returns a UTC `datetime`.
558
+ For this example:
559
+
560
+ ```python
561
+ from datetime import datetime, timezone
562
+
563
+ seconds, nanos = [1657579144, 285000000]
564
+ timestamp = datetime.fromtimestamp(
565
+ timestamp=seconds + nanos / 1e9, tz=timezone.utc
566
+ )
567
+ ```
568
+
569
+ ## Enums
570
+
571
+ ### Status
572
+
573
+ | API value | Name | Open |
574
+ |-----------|------|------|
575
+ | 1 | NEW | Yes |
576
+ | 2 | ASSIGNED | Yes |
577
+ | 3 | ACCEPTED | Yes |
578
+ | 4 | FIXED | No |
579
+ | 5 | VERIFIED | No |
580
+ | 6 | NOT_REPRODUCIBLE | No |
581
+ | 7 | INTENDED_BEHAVIOR | No |
582
+ | 8 | OBSOLETE | No |
583
+ | 9 | INFEASIBLE | No |
584
+ | 10 | DUPLICATE | No |
585
+
586
+ ### Priority and severity
587
+
588
+ Both use values 1 through 5 in the API. The parser subtracts 1 to match the
589
+ Python enums.
590
+
591
+ | API value | Priority | Severity |
592
+ |-----------|----------|----------|
593
+ | 1 | P0 | S0 |
594
+ | 2 | P1 | S1 |
595
+ | 3 | P2 | S2 |
596
+ | 4 | P3 | S3 |
597
+ | 5 | P4 | S4 |
598
+
599
+ ### Issue type
600
+
601
+ | API value | Name |
602
+ |-----------|------|
603
+ | 1 | BUG |
604
+ | 2 | FEATURE_REQUEST |
605
+ | 3 | CUSTOMER_ISSUE |
606
+ | 4 | INTERNAL_CLEANUP |
607
+ | 5 | PROCESS |
608
+ | 6 | VULNERABILITY |
609
+
610
+ ## Update entries
611
+
612
+ Each update uses this 10-position layout:
613
+
614
+ | Index | Field | Type | Meaning |
615
+ |-------|-------|------|---------|
616
+ | `[0]` | author | user array | Person who made the update |
617
+ | `[1]` | timestamp | timestamp | Update time |
618
+ | `[2]` | comment | `list \| null` | [Comment array](#comment-arrays), if present |
619
+ | `[3]` | sequence_number | `int` | Update sequence |
620
+ | `[4]` | unknown | Unknown | Not parsed |
621
+ | `[5]` | field_changes | `list` | [Field change entries](#field-changes) |
622
+ | `[6]` | comment_number | `int` | Raw comment number |
623
+ | `[7]` | attachments | `list` | Attachment entries |
624
+ | `[8]` | unknown | Unknown | Not parsed |
625
+ | `[9]` | issue_id | `int` | Parent issue |
626
+
627
+ Attachment arrays are at `data[0][1][0][i][7]`. Each starts with:
628
+
629
+ ```text
630
+ [attachment_id, mime_type, size_bytes, filename, ...]
631
+ ```
632
+
633
+ The parser reads the restriction level at `attachment[9][0][0]`.
634
+ The `AttachmentRestriction` enum maps 1 to `NO_RESTRICTION`, 2 to `RESTRICTED`,
635
+ and 3 to `RESTRICTED_PLUS`.
636
+
637
+ ## Comment arrays
638
+
639
+ The parser reads comment fields through index `[18]`. Captured arrays can
640
+ omit trailing fields; the parser returns `None` for missing timestamps and
641
+ user fields.
642
+
643
+ | Index | Field | Type | Meaning |
644
+ |-------|-------|------|---------|
645
+ | `[0]` | body | `str` | Comment text |
646
+ | `[2]` | author | user array | Comment author |
647
+ | `[3]` | modified_at | timestamp | Last edit time; exposed as `Comment.timestamp` |
648
+ | `[4]` | unknown | `list` | Recorded value: `[]` |
649
+ | `[5]` | issue_id | `int` | Parent issue |
650
+ | `[6]` | sequence_number | `int` | Starts at 0 in updates and 1 in `listComments` |
651
+ | `[8]` | unknown | `int` | Recorded values: 1 and 2; meaning unverified |
652
+ | `[9]` | unknown | `list` | Recorded value: `[[1]]`; meaning unverified |
653
+ | `[14]` | comment_token | `str` | Recorded as a stable token per comment; not parsed |
654
+ | `[17]` | last_editor | user array | Last person to edit the comment |
655
+ | `[18]` | created_at | timestamp | Original post time |
656
+
657
+ The parser adds 1 to sequence numbers from `/updates` and preserves those
658
+ from `/listComments`. Both produce `Comment.comment_number` values starting
659
+ at 1.
660
+
661
+ Recorded tests found that `[3]` equals `[18]` for unedited comments and is
662
+ later after an edit. A self-edit can change `[3]` while leaving the author
663
+ and last editor equal.
664
+
665
+ ## Field changes
666
+
667
+ Changes are stored at `update[5]`. Each entry has this shape:
668
+
669
+ ```text
670
+ [FIELD_NAME, null, OLD_VALUE_WRAPPER, NEW_VALUE_WRAPPER]
671
+ ```
672
+
673
+ | Index | Field | Meaning |
674
+ |-------|-------|---------|
675
+ | `[0]` | field_name | Changed field |
676
+ | `[2]` | old_value | Previous value in a typed wrapper |
677
+ | `[3]` | new_value | New value in a typed wrapper |
678
+
679
+ An integer wrapper looks like this:
680
+
681
+ ```json
682
+ ["type.googleapis.com/google.protobuf.Int32Value", [42]]
683
+ ```
684
+
685
+ Recorded field names include `component_id`, `type`, `status`, `priority`,
686
+ `hotlist_ids`, `ccs`, `found_in_versions`, `is_archived`, `is_deleted`, and
687
+ `access_limit`.
688
+
689
+ The parser extracts only the field name. `FieldChange.old_value` and
690
+ `FieldChange.new_value` remain `None`.
691
+
692
+ ## Pagination
693
+
694
+ Search, comment, and update responses include a next page token and a total
695
+ count. Search totals are approximate. A `null` token marks the last page.
696
+
697
+ | Request | Where to send the next token |
698
+ |---------|-----------------------------|
699
+ | Search | Request `[6][3]` |
700
+ | Comments | Request `[3]` |
701
+ | Updates, extended form | Request `[3]` |
702
+
703
+ Pass tokens back unchanged. In the client, use
704
+ `next_page(result=search_result)` for searches. For comments, call
705
+ `comments()` with the same issue ID and the result's `next_page_token` as
706
+ `page_token`. The `issue_updates()` method sends one request using the short
707
+ form; it exposes the returned token but does not accept a page token.
708
+
709
+ ## Component hierarchy
710
+
711
+ Recorded component lookups place the public trackers under component
712
+ `166797`:
713
+
714
+ ```text
715
+ Public Trackers (166797)
716
+ ├── Chromium (1362134)
717
+ │ └── Chromium root (1363614)
718
+ │ └── Internals (1456292)
719
+ │ └── Crypto (1768937)
720
+ └── Fuchsia (1360843)
721
+ └── Hardware Platform (1620976)
722
+ └── Zircon Kernel (1478131)
723
+ └── VM (1477815)
724
+ ```
725
+
726
+ Issues contain a numeric `component_id`. Resolve its name and parent path
727
+ with `GET /action/components/{id}`, or fetch several components with
728
+ `GET /action/components?id=X&id=Y`.