datatorch 0.6.2__tar.gz → 0.7.1__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 (112) hide show
  1. {datatorch-0.6.2 → datatorch-0.7.1}/PKG-INFO +2 -2
  2. {datatorch-0.6.2 → datatorch-0.7.1}/README.md +1 -1
  3. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/__init__.py +11 -7
  4. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/client.py +14 -0
  5. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/monitoring.py +27 -2
  6. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/runner/runner.py +11 -0
  7. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/api.py +15 -0
  8. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/client.py +5 -2
  9. datatorch-0.7.1/datatorch/api/entity/dataset.py +103 -0
  10. datatorch-0.7.1/datatorch/api/pull.py +584 -0
  11. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/scripts/import_coco.py +4 -4
  12. datatorch-0.7.1/datatorch/cli/agent/__init__.py +32 -0
  13. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/agent/create.py +8 -6
  14. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/agent/dir.py +1 -1
  15. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/agent/start.py +1 -1
  16. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/groups.py +4 -1
  17. datatorch-0.7.1/datatorch/cli/main/pull.py +151 -0
  18. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/core/settings.py +4 -1
  19. datatorch-0.7.1/datatorch/utils/url.py +9 -0
  20. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch.egg-info/PKG-INFO +2 -2
  21. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch.egg-info/SOURCES.txt +2 -0
  22. {datatorch-0.6.2 → datatorch-0.7.1}/setup.py +1 -1
  23. datatorch-0.6.2/datatorch/api/entity/dataset.py +0 -48
  24. datatorch-0.6.2/datatorch/cli/agent/__init__.py +0 -15
  25. datatorch-0.6.2/datatorch/utils/url.py +0 -5
  26. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/__init__.py +0 -0
  27. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/agent.py +0 -0
  28. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/directory.py +0 -0
  29. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/log_handler.py +0 -0
  30. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/logging.py +0 -0
  31. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/__init__.py +0 -0
  32. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/action/__init__.py +0 -0
  33. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/action/action.py +0 -0
  34. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/action/cache.py +0 -0
  35. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/action/config.py +0 -0
  36. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/job/__init__.py +0 -0
  37. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/job/job.py +0 -0
  38. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/pipeline.py +0 -0
  39. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/resolver.py +0 -0
  40. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/runner/__init__.py +0 -0
  41. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/runner/command.py +0 -0
  42. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/runner/docker.py +0 -0
  43. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/runner/factory.py +0 -0
  44. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/runner/node.py +0 -0
  45. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/runner/python.py +0 -0
  46. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/runner/shell.py +0 -0
  47. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/step/__init__.py +0 -0
  48. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/step/step.py +0 -0
  49. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/pipelines/template.py +0 -0
  50. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/services/__init__.py +0 -0
  51. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/utils/__init__.py +0 -0
  52. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/utils/docker.py +0 -0
  53. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/agent/utils/process.py +0 -0
  54. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/__init__.py +0 -0
  55. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/__init__.py +0 -0
  56. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/annotation.py +0 -0
  57. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/base.py +0 -0
  58. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/file.py +0 -0
  59. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/label.py +0 -0
  60. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/project.py +0 -0
  61. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/settings.py +0 -0
  62. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/sources/__init__.py +0 -0
  63. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/sources/image/__init__.py +0 -0
  64. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/sources/image/bounding_box.py +0 -0
  65. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/sources/image/segmentations.py +0 -0
  66. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/sources/image/typings.py +0 -0
  67. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/sources/source.py +0 -0
  68. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/storage_link.py +0 -0
  69. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/entity/user.py +0 -0
  70. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/scripts/__init__.py +0 -0
  71. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/scripts/import_pixmask.py +0 -0
  72. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/scripts/utils/__init__.py +0 -0
  73. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/scripts/utils/simplify.py +0 -0
  74. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/utils.py +0 -0
  75. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/api/where.py +0 -0
  76. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/__init__.py +0 -0
  77. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/action/__init__.py +0 -0
  78. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/action/create.py +0 -0
  79. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/action/pull.py +0 -0
  80. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/action/run.py +0 -0
  81. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/import_cmds/__init__.py +0 -0
  82. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/import_cmds/coco.py +0 -0
  83. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/import_cmds/pixmask.py +0 -0
  84. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/main/__init__.py +0 -0
  85. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/main/login.py +0 -0
  86. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/main/logout.py +0 -0
  87. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/main/upgrade.py +0 -0
  88. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/main/version.py +0 -0
  89. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/pipeline/__init__.py +0 -0
  90. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/pipeline/generate.py +0 -0
  91. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/pipeline/run.py +0 -0
  92. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/pipeline/upload.py +0 -0
  93. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/spinner.py +0 -0
  94. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/upload/__init__.py +0 -0
  95. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/upload/folder.py +0 -0
  96. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/cli/utils.py +0 -0
  97. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/core/__init__.py +0 -0
  98. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/core/env.py +0 -0
  99. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/core/folder.py +0 -0
  100. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/utils/__init__.py +0 -0
  101. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/utils/converters.py +0 -0
  102. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/utils/files.py +0 -0
  103. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/utils/hash_table.py +0 -0
  104. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/utils/objects.py +0 -0
  105. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/utils/package.py +0 -0
  106. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch/utils/string_style.py +0 -0
  107. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch.egg-info/dependency_links.txt +0 -0
  108. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch.egg-info/entry_points.txt +0 -0
  109. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch.egg-info/not-zip-safe +0 -0
  110. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch.egg-info/requires.txt +0 -0
  111. {datatorch-0.6.2 → datatorch-0.7.1}/datatorch.egg-info/top_level.txt +0 -0
  112. {datatorch-0.6.2 → datatorch-0.7.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datatorch
3
- Version: 0.6.2
3
+ Version: 0.7.1
4
4
  Summary: A CLI and library for interacting with DataTorch.
5
5
  Home-page: https://github.com/datatorch/python
6
6
  Author: DataTorch
@@ -95,4 +95,4 @@ python3 -m venv env
95
95
 
96
96
  rm -rf env
97
97
 
98
- source env/bin/activate && pip3 install dist/datatorch-0.6.1-py3-none-any.whl && pip3 install pycocotools
98
+ source env/bin/activate && pip3 install dist/datatorch-0.6.3-py3-none-any.whl && pip3 install pycocotools
@@ -45,4 +45,4 @@ python3 -m venv env
45
45
 
46
46
  rm -rf env
47
47
 
48
- source env/bin/activate && pip3 install dist/datatorch-0.6.1-py3-none-any.whl && pip3 install pycocotools
48
+ source env/bin/activate && pip3 install dist/datatorch-0.6.3-py3-none-any.whl && pip3 install pycocotools
@@ -12,8 +12,7 @@ from .agent import Agent, tasks
12
12
  from gql import Client as GqlClient
13
13
  from gql.transport.exceptions import TransportClosed, TransportServerError
14
14
  from gql.transport.websockets import WebsocketsTransport
15
- from websockets.exceptions import InvalidMessage, InvalidURI
16
- from websockets import ConnectionClosedError
15
+ from websockets.exceptions import ConnectionClosed, InvalidMessage, InvalidURI
17
16
 
18
17
  from datatorch.api import Client as DtClient
19
18
  from datatorch.utils.package import get_version
@@ -110,12 +109,17 @@ async def start() -> None:
110
109
  await Agent.run(session)
111
110
 
112
111
  except (
113
- ConnectionClosedError,
114
- ConnectionRefusedError,
112
+ # ConnectionClosed covers both the error and the "OK" close
113
+ # (a server-side restart closes cleanly); OSError covers the
114
+ # socket-level failures seen right after a laptop wakes
115
+ # (DNS gaierror, ConnectionReset, ConnectionRefused, timeouts).
116
+ ConnectionClosed,
117
+ OSError,
115
118
  IncompleteReadError,
116
119
  TransportServerError,
117
120
  TransportClosed,
118
121
  InvalidMessage,
122
+ asyncio.TimeoutError,
119
123
  ) as e:
120
124
  await _exit_jobs()
121
125
  await _close_transport(transport)
@@ -139,12 +143,12 @@ async def stop() -> None:
139
143
 
140
144
  print(" ")
141
145
 
142
- logger.warning("Gracefully exiting agent.")
146
+ logger.warning("Gracefully exiting runner.")
143
147
 
144
- logger.info("Closing agent jobs.")
148
+ logger.info("Closing runner jobs.")
145
149
  await _exit_jobs()
146
150
 
147
- logger.info("Closing agent jobs.")
151
+ logger.info("Closing runner jobs.")
148
152
  await _exit_jobs()
149
153
 
150
154
  logger.info("Closing all other tasks.")
@@ -4,6 +4,9 @@ from .directory import agent_directory
4
4
  from gql.client import AsyncClientSession
5
5
 
6
6
  from typing_extensions import TypedDict
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
7
10
 
8
11
 
9
12
  class Log(TypedDict):
@@ -236,6 +239,17 @@ class AgentApiClient(object):
236
239
  # fmt: on
237
240
  return self.execute(mutate, params={"id": step_id, "logs": logs})
238
241
 
242
+ async def force_reconnect(self) -> None:
243
+ """Closes the websocket so the steps subscription raises and the
244
+ outer loop in `agent.start()` reconnects with backoff."""
245
+ transport = getattr(self.session, "transport", None)
246
+ if transport is None or getattr(transport, "websocket", None) is None:
247
+ return
248
+ try:
249
+ await transport.close()
250
+ except Exception as e:
251
+ logger.debug(f"Transport close raised while forcing reconnect: {e}")
252
+
239
253
  async def execute(self, query, *args, params: dict = {}, **kwargs) -> dict:
240
254
  """Wrapper around execute"""
241
255
  removed_none = dict((k, v) for k, v in params.items() if v is not None)
@@ -67,10 +67,17 @@ class AgentSystemStats(object):
67
67
  print_stats(stats)
68
68
  return stats
69
69
 
70
+ # Consecutive failed reports before the connection is considered dead.
71
+ # The steps subscription can sit silently on a half-open socket (e.g.
72
+ # after sleep); the report failing is the signal that reaches us.
73
+ MAX_REPORT_FAILURES = 3
74
+ REPORT_TIMEOUT = 30
75
+
70
76
  def __init__(self, agent, sample_rate=60):
71
77
  self.agent = agent
72
78
  self.sample_rate = sample_rate
73
79
  self.sample = 0
80
+ self.report_failures = 0
74
81
 
75
82
  async def start(self):
76
83
  try:
@@ -90,7 +97,25 @@ class AgentSystemStats(object):
90
97
  while True:
91
98
  self.sample += 1
92
99
  stats = self.stats()
93
- loop = asyncio.get_running_loop()
94
100
  if self.sample != 1:
95
- loop.create_task(self.agent.api.metrics(stats))
101
+ await self._report(stats)
96
102
  await asyncio.sleep(self.sample_rate)
103
+
104
+ async def _report(self, stats):
105
+ """Sends one metric report; repeated failures force a reconnect."""
106
+ try:
107
+ await asyncio.wait_for(
108
+ self.agent.api.metrics(stats), timeout=self.REPORT_TIMEOUT
109
+ )
110
+ self.report_failures = 0
111
+ except asyncio.CancelledError:
112
+ raise
113
+ except Exception as e:
114
+ self.report_failures += 1
115
+ logger.warning(
116
+ f"Metric report failed ({self.report_failures}/{self.MAX_REPORT_FAILURES}): {e}"
117
+ )
118
+ if self.report_failures >= self.MAX_REPORT_FAILURES:
119
+ logger.error("Connection looks dead; forcing a reconnect.")
120
+ self.report_failures = 0
121
+ await self.agent.api.force_reconnect()
@@ -14,6 +14,12 @@ class ProcessCodeError(Exception):
14
14
  pass
15
15
 
16
16
 
17
+ # Max size of one stdout line from an action subprocess. Output values
18
+ # arrive as one `::var::<json>` line, so this bounds the largest single
19
+ # output value (e.g. an http-request body), not ordinary log lines.
20
+ STDOUT_LINE_LIMIT = 16 * 1024 * 1024
21
+
22
+
17
23
  class Runner(object):
18
24
  def __init__(self, config: dict, action: "Action"):
19
25
  self.config = config
@@ -110,6 +116,11 @@ class Runner(object):
110
116
  stdout=asyncio.subprocess.PIPE,
111
117
  stderr=asyncio.subprocess.STDOUT,
112
118
  env={**os.environ, **(env or {})},
119
+ # Outputs are reported as single `::var::<json>` stdout lines
120
+ # (see check_for_output), so a line can carry an entire fetched
121
+ # page body — asyncio's 64KB default readline limit kills the
122
+ # stream with LimitOverrunError on anything larger.
123
+ limit=STDOUT_LINE_LIMIT,
113
124
  )
114
125
  if wait:
115
126
  await process.wait()
@@ -56,6 +56,14 @@ _FILE = File.add_fragment("""
56
56
  }
57
57
  """)
58
58
 
59
+ _DATASET = Dataset.add_fragment("""
60
+ query GetDataset($id: ID!) {
61
+ dataset: datasetById(id: $id) {
62
+ ...DatasetFields
63
+ }
64
+ }
65
+ """)
66
+
59
67
  _VIEWER = User.add_fragment("""
60
68
  query GetViewer {
61
69
  viewer {
@@ -119,6 +127,13 @@ class ApiClient(Client):
119
127
  File, self.query_to_class(File, _FILE, path="file", params={"fileId": id})
120
128
  )
121
129
 
130
+ def dataset(self, id: str) -> Dataset:
131
+ """Retrieve a dataset (or branch) by ID"""
132
+ return cast(
133
+ Dataset,
134
+ self.query_to_class(Dataset, _DATASET, path="dataset", params={"id": id}),
135
+ )
136
+
122
137
  def upload_to_default_filesource(
123
138
  self,
124
139
  project: Project,
@@ -162,7 +162,8 @@ class Client(object):
162
162
  dl_file = os.path.join(directory, name)
163
163
  dl_file = os.path.abspath(dl_file)
164
164
 
165
- if os.path.isfile(dl_file) & skip == True:
165
+ # `isfile(x) & skip == True` only worked by bitwise-bool accident
166
+ if skip and os.path.isfile(dl_file):
166
167
  return dl_file, None
167
168
 
168
169
  query_string = urlencode({"download": "true", "stream": "true"})
@@ -172,8 +173,10 @@ class Client(object):
172
173
  result = requests.get(
173
174
  download_url, headers={self.token_header: self._api_token}, stream=True
174
175
  )
176
+ # Never write an error body to disk as a "downloaded file"
177
+ result.raise_for_status()
175
178
 
176
- content = result.headers["content-disposition"]
179
+ content = result.headers.get("content-disposition", "")
177
180
  # _, value = cgi.parse_header(content)
178
181
  parser = HeaderParser()
179
182
  headers = parser.parsestr(f"Content-Disposition: {content}")
@@ -0,0 +1,103 @@
1
+ from .base import BaseEntity
2
+
3
+ __all__ = "Dataset"
4
+
5
+
6
+ _FORK_DATASET = """
7
+ mutation ForkDataset(
8
+ $datasetId: ID!
9
+ $branchName: String!
10
+ $versionId: ID
11
+ ) {
12
+ fork: forkDataset(
13
+ datasetId: $datasetId
14
+ branchName: $branchName
15
+ versionId: $versionId
16
+ ) {
17
+ dataset {
18
+ id
19
+ name
20
+ description
21
+ projectId
22
+ branchName
23
+ parentDatasetId
24
+ forkedAt
25
+ }
26
+ }
27
+ }
28
+ """
29
+
30
+
31
+ _CREATE_DATASET = """
32
+ mutation CreateDataset(
33
+ $projectId: ID!
34
+ $name: String!
35
+ $description: String
36
+ ) {
37
+ dataset: createDataset(
38
+ input: {
39
+ projectId: $projectId
40
+ name: $name
41
+ description: $description
42
+ }
43
+ ) {
44
+ id
45
+ }
46
+ }
47
+ """
48
+
49
+
50
+ class Dataset(BaseEntity):
51
+ id: str
52
+ name: str
53
+ description: str
54
+ project_id: str
55
+ kilobytes: int
56
+ formatted_bytes: int
57
+ created_at: str
58
+ updated_at: str
59
+
60
+ def create(self, client=None):
61
+ super().create(client=client)
62
+
63
+ assert self.project_id is not None
64
+ results = self.client.execute(
65
+ _CREATE_DATASET,
66
+ params={
67
+ "projectId": self.project_id,
68
+ "name": self.name,
69
+ "description": self.description,
70
+ },
71
+ )
72
+
73
+ self.id = results.get("dataset").get("id")
74
+
75
+ def fork(self, branch_name: str, version_id: str = None, client=None):
76
+ """
77
+ Forks this dataset into a branch and returns it as a new Dataset.
78
+
79
+ The branch shares the parent's storage objects and copies its
80
+ annotations. Pass `version_id` to start the branch from a version's
81
+ reconstructed state instead of the live state. The returned dataset
82
+ can be used anywhere a dataset id is accepted (e.g. uploads).
83
+
84
+ Note: branch attributes (`branch_name`, `parent_dataset_id`,
85
+ `forked_at`) are set dynamically on the returned instance; they are
86
+ deliberately kept out of the class annotations so the generated
87
+ `DatasetFields` fragment stays compatible with servers that predate
88
+ branching.
89
+ """
90
+ assert self.id is not None
91
+ if client:
92
+ self.client = client
93
+ assert self.client is not None
94
+
95
+ results = self.client.execute(
96
+ _FORK_DATASET,
97
+ params={
98
+ "datasetId": self.id,
99
+ "branchName": branch_name,
100
+ "versionId": version_id,
101
+ },
102
+ )
103
+ return Dataset(results.get("fork").get("dataset"), client=self.client)