projectaria-debug-sharing 0.1.0__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.
@@ -0,0 +1,327 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import json
19
+ import logging
20
+ import os
21
+ import re
22
+ from collections.abc import Collection
23
+ from typing import Any, Final
24
+
25
+ import aiohttp
26
+ from projectaria_commons.commons.http_helper import HttpHelper
27
+ from projectaria_debug_sharing.debug_sharing.submitter import SubmissionError, Submitter
28
+ from projectaria_debug_sharing.debug_sharing.types import (
29
+ Attachment,
30
+ DebugReport,
31
+ ERROR_CODE_KEY,
32
+ VRS_RECORDING_ID_KEY,
33
+ )
34
+ from projectaria_login import ProjectAriaLogin
35
+
36
+ logger: logging.Logger = logging.getLogger(__name__)
37
+
38
+ REPORT_BUG_URL: Final[str] = "https://graph.oculus.com/report_bug"
39
+
40
+ DEV_SERVER_ENV: Final[str] = "PROJECTARIA_DEV_SERVER"
41
+ """Names a test server to send reports to instead of production: `NAME` sends to
42
+ `graph.NAME.oculus.com`. Unset for everyone else."""
43
+
44
+ _SANDBOX_NAME: Final[re.Pattern[str]] = re.compile(r"[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*")
45
+
46
+ CATEGORY_ID: Final[str] = "113915505441756"
47
+ """`PRODUCT_NONE_FBID`: the user had no chance to select a category, no UI being
48
+ shown. The endpoint's only required parameter, so it cannot be omitted, but
49
+ categories are being removed platform-wide and nothing may branch on it."""
50
+
51
+ ENTRY_SOURCE: Final[str] = "projectaria_debug_sharing"
52
+
53
+ CONNECT_TIMEOUT_SEC: Final[float] = 10.0
54
+
55
+ READ_TIMEOUT_SEC: Final[float] = 60.0
56
+ """How long FlyTrap may stay silent once the whole report is sent. Longer than
57
+ the server's own 30 s limit, so a slow ingest is not mistaken for a dead link."""
58
+
59
+ TOTAL_TIMEOUT_SEC: Final[float] = 1800.0
60
+ """Cap on one whole submission, so an upload that trickles along without ever
61
+ tripping `READ_TIMEOUT_SEC` still ends. Well under the spool's one-hour claim
62
+ expiry, so a stalled upload cannot outlive its claim and be sent twice."""
63
+
64
+ _PERMANENT_SERVER_ERRORS: Final[frozenset[int]] = frozenset({501, 505})
65
+ """501 Not Implemented and 505 HTTP Version Not Supported: a retry gets the
66
+ same answer."""
67
+
68
+ _REPORT_FIELDS: Final[frozenset[str]] = frozenset(
69
+ {
70
+ "category_id",
71
+ "description",
72
+ "source_app",
73
+ "entry_source",
74
+ "error_description",
75
+ "session_id",
76
+ "custom_app_metadata",
77
+ "app_id",
78
+ "vrs_recording_id",
79
+ }
80
+ )
81
+
82
+
83
+ class FlyTrapSubmissionError(SubmissionError):
84
+ """FlyTrap refused a report, or could not be reached."""
85
+
86
+
87
+ class NotLoggedInError(FlyTrapSubmissionError):
88
+ """Nobody is logged in to Project Aria, so there is no token to send with."""
89
+
90
+
91
+ class FlyTrapSubmitter(Submitter):
92
+ """
93
+ Files a `DebugReport` as a FlyTrap bug through OCGraphReportBugPost.
94
+ """
95
+
96
+ def __init__(self, *, login: ProjectAriaLogin) -> None:
97
+ """
98
+ Args:
99
+ login: Supplies the token and app ID. Resumed on every submit, so a
100
+ report sent hours later still uses a current token.
101
+ """
102
+ self._login = login
103
+
104
+ async def submit(self, report: DebugReport) -> str:
105
+ """
106
+ Args:
107
+ report: The report to file.
108
+
109
+ Returns:
110
+ The FlyTrap bug ID.
111
+
112
+ Raises:
113
+ FlyTrapSubmissionError: The report was not filed. `retryable` says
114
+ whether sending it again could succeed.
115
+ """
116
+ token = await self._get_current_token()
117
+ form = self._build_form(report)
118
+
119
+ async with HttpHelper() as http:
120
+ http.set_auth_token(token)
121
+ payload = await self._post(http.session, form)
122
+
123
+ return self._bug_id_from(payload)
124
+
125
+ async def _get_current_token(self) -> str:
126
+ """
127
+ Raises:
128
+ FlyTrapSubmissionError: Nobody is logged in. Retryable, because
129
+ logging in is what makes the next attempt succeed.
130
+ """
131
+ await self._login.resume()
132
+ token = self._login.current_token
133
+ if token is None:
134
+ raise NotLoggedInError(
135
+ "No Project Aria login, so the report cannot be sent. "
136
+ "Log in and submit again; the report is kept.",
137
+ retryable=True,
138
+ )
139
+ return token
140
+
141
+ def _build_form(self, report: DebugReport) -> aiohttp.FormData:
142
+ form = aiohttp.FormData()
143
+ for name, value in self._report_fields(report).items():
144
+ form.add_field(name, value)
145
+ for part_name, attachment in _attachments_by_part_name(
146
+ report.attachments
147
+ ).items():
148
+ form.add_field(
149
+ part_name,
150
+ attachment.data,
151
+ filename=attachment.filename or attachment.name,
152
+ content_type=attachment.content_type,
153
+ )
154
+ return form
155
+
156
+ def _report_fields(self, report: DebugReport) -> dict[str, str]:
157
+ """
158
+ `app_id` is the login's, so it names the app that actually filed the
159
+ report. `vrs_recording_id` is sent only when the report names one.
160
+ """
161
+ fields = {
162
+ "category_id": CATEGORY_ID,
163
+ "description": report.description,
164
+ "source_app": report.source_app,
165
+ "entry_source": ENTRY_SOURCE,
166
+ "error_description": report.metadata.get(ERROR_CODE_KEY, ""),
167
+ "session_id": report.report_id,
168
+ "custom_app_metadata": json.dumps(dict(report.metadata)),
169
+ }
170
+ app_id = self._login.current_app_id
171
+ if app_id is not None:
172
+ fields["app_id"] = app_id
173
+ recording_id = report.metadata.get(VRS_RECORDING_ID_KEY)
174
+ if recording_id:
175
+ fields["vrs_recording_id"] = recording_id
176
+ return fields
177
+
178
+ async def _post(
179
+ self, session: aiohttp.ClientSession, form: aiohttp.FormData
180
+ ) -> dict[str, Any]:
181
+ """
182
+ Sends the form and returns FlyTrap's JSON response. Status is checked
183
+ here rather than by HttpHelper's session, which would discard the body
184
+ that says why FlyTrap refused.
185
+
186
+ Raises:
187
+ FlyTrapSubmissionError: On an HTTP error, a malformed body or a
188
+ transport failure. A read timeout is not retryable: the whole
189
+ report had been sent, so FlyTrap may have filed it.
190
+ """
191
+ url = report_bug_url()
192
+ timeout = aiohttp.ClientTimeout(
193
+ total=TOTAL_TIMEOUT_SEC,
194
+ sock_connect=CONNECT_TIMEOUT_SEC,
195
+ sock_read=READ_TIMEOUT_SEC,
196
+ )
197
+ try:
198
+ async with session.post(
199
+ url, data=form, timeout=timeout, raise_for_status=False
200
+ ) as response:
201
+ body = await response.text()
202
+ if response.status >= 400:
203
+ raise FlyTrapSubmissionError(
204
+ f"FlyTrap returned HTTP {response.status}: {body[:500]}",
205
+ retryable=_is_retryable_status(response.status),
206
+ )
207
+ return self._parse(body)
208
+ except aiohttp.SocketTimeoutError as e:
209
+ raise FlyTrapSubmissionError(
210
+ f"FlyTrap did not answer within {READ_TIMEOUT_SEC:g}s. The report "
211
+ "may have been filed, so it is not sent again.",
212
+ retryable=False,
213
+ ) from e
214
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
215
+ raise FlyTrapSubmissionError(
216
+ f"Could not reach FlyTrap: {_describe(e)}", retryable=True
217
+ ) from e
218
+
219
+ def _parse(self, body: str) -> dict[str, Any]:
220
+ """
221
+ Raises:
222
+ FlyTrapSubmissionError: The body is not a JSON object. Not retryable.
223
+ """
224
+ try:
225
+ payload = json.loads(body)
226
+ except json.JSONDecodeError as e:
227
+ raise FlyTrapSubmissionError(
228
+ f"FlyTrap returned a non-JSON body: {body[:500]}", retryable=False
229
+ ) from e
230
+ if not isinstance(payload, dict):
231
+ raise FlyTrapSubmissionError(
232
+ f"FlyTrap returned {type(payload).__name__}, expected an object",
233
+ retryable=False,
234
+ )
235
+ return payload
236
+
237
+ def _bug_id_from(self, payload: dict[str, Any]) -> str:
238
+ """
239
+ Raises:
240
+ FlyTrapSubmissionError: FlyTrap refused the report, or reported
241
+ success without a `bug_id`. A refusal is retryable only when
242
+ FlyTrap says so and did not prohibit the report; its message
243
+ carries the whole response, since that is where the trace ID
244
+ is. A success without a `bug_id` is not retried: the report
245
+ probably landed, and resending would file a duplicate.
246
+ """
247
+ if not payload.get("success"):
248
+ prohibited = bool(payload.get("is_prohibited"))
249
+ raise FlyTrapSubmissionError(
250
+ f"{payload.get('error_message') or 'FlyTrap rejected the report'} "
251
+ f"-- full response: {json.dumps(payload, sort_keys=True)[:1000]}",
252
+ retryable=bool(payload.get("is_retryable")) and not prohibited,
253
+ )
254
+
255
+ bug_id = payload.get("bug_id")
256
+ if not bug_id:
257
+ raise FlyTrapSubmissionError(
258
+ "FlyTrap reported success but returned no bug_id", retryable=False
259
+ )
260
+
261
+ logger.debug("Submitted debug report as FlyTrap bug %s", bug_id)
262
+ return str(bug_id)
263
+
264
+
265
+ def report_bug_url() -> str:
266
+ """
267
+ Production, or the test server `PROJECTARIA_DEV_SERVER` names. Only a name made
268
+ of host labels is accepted, so the report and its token can only go to an
269
+ oculus.com host, over https.
270
+
271
+ Raises:
272
+ FlyTrapSubmissionError: `PROJECTARIA_DEV_SERVER` is not a server name.
273
+ Not retryable: a kept report does not record where it was meant to
274
+ go, so a later drain without the variable would send it to
275
+ production.
276
+ """
277
+ sandbox = os.environ.get(DEV_SERVER_ENV)
278
+ if not sandbox:
279
+ return REPORT_BUG_URL
280
+ if not _SANDBOX_NAME.fullmatch(sandbox):
281
+ raise FlyTrapSubmissionError(
282
+ f"{DEV_SERVER_ENV}={sandbox!r} is not a host name",
283
+ retryable=False,
284
+ )
285
+ url = f"https://graph.{sandbox}.oculus.com/report_bug"
286
+ logger.debug("Sending to %s, not production: %s is set", url, DEV_SERVER_ENV)
287
+ return url
288
+
289
+
290
+ def _attachments_by_part_name(
291
+ attachments: Collection[Attachment],
292
+ ) -> dict[str, Attachment]:
293
+ """
294
+ Gives every attachment a multipart field name of its own, so the server
295
+ keeps all of them.
296
+ """
297
+ parts: dict[str, Attachment] = {}
298
+ for attachment in attachments:
299
+ parts[_unique_part_name(attachment.name, taken=parts.keys())] = attachment
300
+ return parts
301
+
302
+
303
+ def _unique_part_name(name: str, *, taken: Collection[str]) -> str:
304
+ """
305
+ `name`, prefixed with `attachment_` if it is a report field's, and given a
306
+ numeric suffix if an earlier attachment already uses it.
307
+ """
308
+ base = f"attachment_{name}" if name in _REPORT_FIELDS else name
309
+ candidate = base
310
+ suffix = 2
311
+ while candidate in taken or candidate in _REPORT_FIELDS:
312
+ candidate = f"{base}_{suffix}"
313
+ suffix += 1
314
+ return candidate
315
+
316
+
317
+ def _is_retryable_status(status: int) -> bool:
318
+ """Whether an HTTP error status is transient: 429, or any 5xx except
319
+ 501 and 505, which reject the request itself."""
320
+ return status == 429 or (
321
+ 500 <= status < 600 and status not in _PERMANENT_SERVER_ERRORS
322
+ )
323
+
324
+
325
+ def _describe(error: BaseException) -> str:
326
+ """The error's message, or its type for one with none (a bare `TimeoutError`)."""
327
+ return str(error) or type(error).__name__
@@ -0,0 +1,103 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """The boundary between assembling a report and delivering it."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import Protocol, runtime_checkable
22
+
23
+ from projectaria_debug_sharing.debug_sharing.types import DebugReport
24
+
25
+
26
+ class SubmissionError(Exception):
27
+ """A report was not delivered."""
28
+
29
+ def __init__(
30
+ self,
31
+ message: str,
32
+ *,
33
+ report: DebugReport | None = None,
34
+ retryable: bool = True,
35
+ ) -> None:
36
+ """
37
+ Args:
38
+ message: Why, in words the user can act on.
39
+ report: Set by whoever assembled the report, so a caller can spool it.
40
+ retryable: Whether sending the same report again could succeed.
41
+ Defaults to True: a wrong True costs one more attempt, a wrong
42
+ False drops a report for good.
43
+ """
44
+ super().__init__(message)
45
+ self.report = report
46
+ self.retryable = retryable
47
+
48
+
49
+ @runtime_checkable
50
+ class Submitter(Protocol):
51
+ """Delivers a report, returns the bug ID. Raises SubmissionError."""
52
+
53
+ async def submit(self, report: DebugReport) -> str: ...
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class Delivery:
58
+ """What happened to one report sent now."""
59
+
60
+ bug_id: str | None = None
61
+ """The filed bug's id; None when the report was not filed."""
62
+
63
+ error: SubmissionError | None = None
64
+ """Why the report was not filed."""
65
+
66
+ spooled: Path | None = None
67
+ """Where the report was kept for a later drain; None when it was filed, was
68
+ refused, or could not be kept."""
69
+
70
+ def __post_init__(self) -> None:
71
+ """
72
+ Raises:
73
+ ValueError: Not exactly one of `bug_id` and `error` is set, or a
74
+ filed report claims to be spooled.
75
+ """
76
+ if (self.bug_id is None) == (self.error is None):
77
+ raise ValueError("a Delivery has exactly one of bug_id and error")
78
+ if self.bug_id is not None and self.spooled is not None:
79
+ raise ValueError("a filed report is not spooled")
80
+
81
+ @property
82
+ def will_retry(self) -> bool:
83
+ """Whether a later drain sends the report: kept, and the failure retryable."""
84
+ return (
85
+ self.spooled is not None and self.error is not None and self.error.retryable
86
+ )
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class DrainResult:
91
+ """What a drain of the kept reports did."""
92
+
93
+ sent: tuple[str, ...] = ()
94
+ """The bug ids of the reports filed."""
95
+
96
+ waiting: int = 0
97
+ """How many reports this drain tried that failed and wait for the next one.
98
+ Reports it skipped, or that another drain was sending, are not counted."""
99
+
100
+ rejected: int = 0
101
+ """How many reports will never be sent: refused by the destination or
102
+ locally, or unreadable on disk. They are kept marked, or deleted if they
103
+ cannot be."""
@@ -0,0 +1,172 @@
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import uuid
18
+ from collections.abc import Awaitable, Callable, Mapping
19
+ from dataclasses import dataclass, field
20
+ from enum import Enum
21
+ from types import MappingProxyType
22
+ from typing import Final
23
+
24
+ VRS_RECORDING_ID_KEY: Final[str] = "vrs_recording_id"
25
+ """`DebugReport.metadata` key naming the VRS recording the report is about. One
26
+ recording only."""
27
+
28
+ ERROR_CODE_KEY: Final[str] = "error_code"
29
+ """`DebugReport.metadata` key for the error code that prompted the report."""
30
+
31
+
32
+ class CollectionScope(Enum):
33
+ """
34
+ A category of data a collector gathers, and the unit consent is given in.
35
+ """
36
+
37
+ LOGS = "logs"
38
+ ENVIRONMENT = "environment"
39
+ DIAGNOSTICS = "diagnostics"
40
+ RECORDING = "recording"
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class Attachment:
45
+ """One file we upload."""
46
+
47
+ name: str
48
+ """Short label, sent as the form field name."""
49
+
50
+ data: bytes
51
+
52
+ content_type: str = "application/octet-stream"
53
+
54
+ filename: str | None = None
55
+ """Filename sent with the part. When None, callers fall back to `name`."""
56
+
57
+
58
+ @dataclass(frozen=True, kw_only=True)
59
+ class DebugReport:
60
+ """
61
+ Everything we send for one bug.
62
+ """
63
+
64
+ description: str
65
+ source_app: str
66
+ attachments: tuple[Attachment, ...] = ()
67
+
68
+ metadata: Mapping[str, str] = field(default_factory=lambda: MappingProxyType({}))
69
+ """Read-only copy: `frozen=True` alone would still let a caller mutate their
70
+ own dict, or the dict behind their `MappingProxyType`, after the consent
71
+ prompt described it. Values become strings, and `None` values are dropped
72
+ rather than sent as `"None"`."""
73
+
74
+ report_id: str = field(default_factory=lambda: str(uuid.uuid4()))
75
+ """Correlates the PC and device halves. Sent as `session_id`, which the
76
+ endpoint copies into `client_server_join_key`."""
77
+
78
+ def __post_init__(self) -> None:
79
+ object.__setattr__(
80
+ self,
81
+ "metadata",
82
+ MappingProxyType(
83
+ {str(k): str(v) for k, v in self.metadata.items() if v is not None}
84
+ ),
85
+ )
86
+
87
+
88
+ class SpooledReportError(Exception):
89
+ """
90
+ A spooled report could not be read. A drain marks it refused, so it is
91
+ never sent, and goes on to the next one.
92
+ """
93
+
94
+
95
+ class ConsentSource(Enum):
96
+ """Where a report's consent came from, recorded on the report."""
97
+
98
+ PROMPTED = "prompted"
99
+ """The user answered the tool's consent prompt."""
100
+
101
+ CALLER = "caller"
102
+ """The calling tool obtained consent itself, for example its own question."""
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class OfferedCollector:
107
+ """One line of the consent prompt, and the collector it speaks for."""
108
+
109
+ name: str
110
+ scope: CollectionScope
111
+ disclosure: str
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class ConsentRequest:
116
+ """What a front end needs to render an accurate consent prompt."""
117
+
118
+ source_app: str
119
+
120
+ offered: tuple[OfferedCollector, ...]
121
+ """Every collector that would run, with the sentence to show for it. Taken
122
+ from the list that does the collecting, so the prompt cannot describe
123
+ something that will not run, or omit something that will."""
124
+
125
+ @property
126
+ def scopes(self) -> frozenset[CollectionScope]:
127
+ """The categories on offer, which is what the user chooses between."""
128
+ return frozenset(o.scope for o in self.offered)
129
+
130
+
131
+ RequestConsent = Callable[
132
+ [ConsentRequest], Awaitable["frozenset[CollectionScope] | None"]
133
+ ]
134
+ """Returns the granted scopes. None means the user declined outright; an empty
135
+ set still sends the description and caller metadata, with no attachments."""
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class CollectorFailure:
140
+ """A collector that raised. Collection is best effort, so these are reported."""
141
+
142
+ collector_name: str
143
+
144
+ error: str
145
+ """The exception's message, or its class name when the message is empty."""
146
+
147
+ error_type: str = ""
148
+ """The exception's class name. Empty when the failure was built without one."""
149
+
150
+
151
+ @dataclass(frozen=True)
152
+ class ShareOutcome:
153
+ """Everything the caller needs to tell the user what happened."""
154
+
155
+ bug_id: str
156
+ report_id: str
157
+
158
+ failures: tuple[CollectorFailure, ...] = field(default_factory=tuple)
159
+ """Collectors that failed. The report was still submitted; the caller should
160
+ say so, so the user knows the bundle is incomplete."""
161
+
162
+
163
+ class ConsentDeclinedError(Exception):
164
+ """The user declined to share. Nothing was collected or sent."""
165
+
166
+
167
+ class NoConsentBasisError(Exception):
168
+ """
169
+ Nothing says the user agreed to this. Raised rather than assuming a grant:
170
+ a missing prompt is far more likely a caller that forgot one than a caller
171
+ that meant to collect everything.
172
+ """
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: projectaria-debug-sharing
3
+ Version: 0.1.0
4
+ Summary: Project Aria Debug Sharing Library
5
+ Home-page: https://www.projectaria.com
6
+ Author: Meta Reality Labs Research
7
+ License: Apache-2.0
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: aiohttp>=3.10
12
+ Requires-Dist: projectaria-commons~=0.1.0
13
+ Requires-Dist: projectaria-login~=0.2.0
14
+ Dynamic: author
15
+ Dynamic: description
16
+ Dynamic: description-content-type
17
+ Dynamic: home-page
18
+ Dynamic: license
19
+ Dynamic: license-file
20
+ Dynamic: requires-dist
21
+ Dynamic: requires-python
22
+ Dynamic: summary
23
+
24
+ Shared debug-sharing library for Project Aria tools.
25
+
26
+ Collects diagnostic logs and environment details, captures the user's consent,
27
+ and submits a correlated bug report, so MPS CLI, Client SDK and Aria Studio can
28
+ offer one consistent reporting experience.
29
+
30
+ For more information, visit [Project Aria](https://www.projectaria.com).
@@ -0,0 +1,22 @@
1
+ projectaria_debug_sharing/__init__.py,sha256=zsf453g7MRWide6tT4tf_CzY-Wr6i-pOiS4C6CZaRb0,749
2
+ projectaria_debug_sharing/debug_sharing/__init__.py,sha256=6xZPTzPtzy-lDojjgRGorJj9GQd2pzDdlt3gzc8rgAU,614
3
+ projectaria_debug_sharing/debug_sharing/cli.py,sha256=8dMxP-NqlfS3IaD3C6aIp8gO8lQuKSa77ElusGBA5Bw,6107
4
+ projectaria_debug_sharing/debug_sharing/collectors.py,sha256=bGtxY0BVuUUDdJXPTXcUrPnwTzg_p-L64Ug3AfD0Vjg,16547
5
+ projectaria_debug_sharing/debug_sharing/command.py,sha256=ObUYs5be8AXKTy9JaB4LNjwmSSe7Ahtvk-4SKg0fQd4,1070
6
+ projectaria_debug_sharing/debug_sharing/facade.py,sha256=kvxa0DUTVX9xE_HdWL5wacVgHlKrpO9-KwFit6Mhn3I,16775
7
+ projectaria_debug_sharing/debug_sharing/flytrap.py,sha256=yjYMp9_T0IZOHKBcviAvA5rpylKtuMYhwxc3dg-EPK8,12081
8
+ projectaria_debug_sharing/debug_sharing/submitter.py,sha256=Yks6KZt5uTLaPWJ9aFHU1wojFJEy4mzdolXDlXg16rQ,3453
9
+ projectaria_debug_sharing/debug_sharing/types.py,sha256=j5LnuyoSPbw3d3VkwEuEzUqBafLbKPVrnaqwiFCt2iA,5269
10
+ projectaria_debug_sharing/debug_sharing/_internal/__init__.py,sha256=ATD5Za7LwP-bGqN8PQsTyTzFrl7ER2QgIs_8NoIhZiU,599
11
+ projectaria_debug_sharing/debug_sharing/_internal/delivery.py,sha256=cJu28RWV9PW3yLocgnfoWDhRVbTQw20N5IyMWrCsclc,9731
12
+ projectaria_debug_sharing/debug_sharing/_internal/file_access.py,sha256=UwrhBTm3DnTtPmytCFGgbIkUYhNIl858PF4l25ysq5Y,4090
13
+ projectaria_debug_sharing/debug_sharing/_internal/manifest.py,sha256=pi91XZmjQEecmUrB5UhSQUsQS-46hOQVd_isCZaWoZg,4836
14
+ projectaria_debug_sharing/debug_sharing/_internal/plain_files.py,sha256=FOnRuOyxQheUE0pf5Q0rgF4x0WMRn2_w7QTCy8fnjxc,6761
15
+ projectaria_debug_sharing/debug_sharing/_internal/report_command.py,sha256=pPkhZD7DLgJfbbXDik3l-PjLoV7P4zKxealdHSHV7tE,21287
16
+ projectaria_debug_sharing/debug_sharing/_internal/spool.py,sha256=Te3lOGj0ltbXvujA4j5gSGJ6cpfdk41YhlaN0OZaenw,25615
17
+ projectaria_debug_sharing-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
+ projectaria_debug_sharing-0.1.0.dist-info/METADATA,sha256=ZnkwrCQaxNJehUnRIB-BzkhDeEeku44LeMPWmUZErkM,929
19
+ projectaria_debug_sharing-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
20
+ projectaria_debug_sharing-0.1.0.dist-info/entry_points.txt,sha256=6RgKvfI0LpOPLv42y9CnY4lCEa--GqA_i7NEzPxdrVo,87
21
+ projectaria_debug_sharing-0.1.0.dist-info/top_level.txt,sha256=SsQLlgUmT4TeA3KS5pq2MuLtJO6yMW-HKlIWC6cxd8c,26
22
+ projectaria_debug_sharing-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aria-debug-report = projectaria_debug_sharing.debug_sharing.cli:main