pyarrow-client 1.0.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.
pyarrow/__version__.py ADDED
@@ -0,0 +1,10 @@
1
+ __title__ = 'pyarrow-client' # Match the PyPI package name
2
+ __version__ = '1.0.0'
3
+ __description__ = 'Official Python Client Library for Arrow Trading API'
4
+ __url__ = 'https://arrow.trade'
5
+ __download_url__ = 'https://pypi.org/project/py-arrow/'
6
+ __license__ = 'MIT'
7
+ __author__ = 'Abhishek Jain'
8
+ __author_email__ = 'abhishek.jain@irage.in'
9
+ __maintainer__ = 'Arrow Trading Team'
10
+ __maintainer_email__ = 'support@arrow.trade'
pyarrow/arrow_utils.py ADDED
@@ -0,0 +1,230 @@
1
+ expiry_dates = {
2
+ "NIFTY": {
3
+ "2018": [
4
+ "25-Jan-2018",
5
+ "22-Feb-2018",
6
+ "28-Mar-2018",
7
+ "29-Mar-2018",
8
+ "26-Apr-2018",
9
+ "31-May-2018",
10
+ "28-Jun-2018",
11
+ "26-Jul-2018",
12
+ "30-Aug-2018",
13
+ "27-Sep-2018",
14
+ "25-Oct-2018",
15
+ "29-Nov-2018",
16
+ "27-Dec-2018"
17
+ ],
18
+ "2019": [
19
+ "31-Jan-2019",
20
+ "14-Feb-2019",
21
+ "21-Feb-2019",
22
+ "28-Feb-2019",
23
+ "07-Mar-2019",
24
+ "14-Mar-2019",
25
+ "20-Mar-2019",
26
+ "28-Mar-2019",
27
+ "04-Apr-2019",
28
+ "11-Apr-2019",
29
+ "18-Apr-2019",
30
+ "25-Apr-2019",
31
+ "02-May-2019",
32
+ "09-May-2019",
33
+ "16-May-2019",
34
+ "23-May-2019",
35
+ "30-May-2019",
36
+ "06-Jun-2019",
37
+ "13-Jun-2019",
38
+ "20-Jun-2019",
39
+ "27-Jun-2019",
40
+ "04-Jul-2019",
41
+ "11-Jul-2019",
42
+ "18-Jul-2019",
43
+ "25-Jul-2019",
44
+ "01-Aug-2019",
45
+ "08-Aug-2019",
46
+ "14-Aug-2019",
47
+ "22-Aug-2019",
48
+ "29-Aug-2019",
49
+ "05-Sep-2019",
50
+ "12-Sep-2019",
51
+ "19-Sep-2019",
52
+ "26-Sep-2019",
53
+ "03-Oct-2019",
54
+ "10-Oct-2019",
55
+ "17-Oct-2019",
56
+ "24-Oct-2019",
57
+ "31-Oct-2019",
58
+ "07-Nov-2019",
59
+ "14-Nov-2019",
60
+ "21-Nov-2019",
61
+ "28-Nov-2019",
62
+ "05-Dec-2019",
63
+ "12-Dec-2019",
64
+ "19-Dec-2019",
65
+ "26-Dec-2019"
66
+ ],
67
+ "2020": [
68
+ "02-Jan-2020",
69
+ "09-Jan-2020",
70
+ "16-Jan-2020",
71
+ "23-Jan-2020",
72
+ "30-Jan-2020",
73
+ "06-Feb-2020",
74
+ "13-Feb-2020",
75
+ "20-Feb-2020",
76
+ "27-Feb-2020",
77
+ "05-Mar-2020",
78
+ "12-Mar-2020",
79
+ "19-Mar-2020",
80
+ "26-Mar-2020",
81
+ "01-Apr-2020",
82
+ "09-Apr-2020",
83
+ "16-Apr-2020",
84
+ "23-Apr-2020",
85
+ "30-Apr-2020",
86
+ "07-May-2020",
87
+ "14-May-2020",
88
+ "21-May-2020",
89
+ "28-May-2020",
90
+ "04-Jun-2020",
91
+ "11-Jun-2020",
92
+ "18-Jun-2020",
93
+ "25-Jun-2020",
94
+ "02-Jul-2020",
95
+ "09-Jul-2020",
96
+ "16-Jul-2020",
97
+ "23-Jul-2020",
98
+ "30-Jul-2020",
99
+ "06-Aug-2020",
100
+ "13-Aug-2020",
101
+ "20-Aug-2020",
102
+ "27-Aug-2020",
103
+ "03-Sep-2020",
104
+ "10-Sep-2020",
105
+ "17-Sep-2020",
106
+ "24-Sep-2020",
107
+ "01-Oct-2020",
108
+ "08-Oct-2020",
109
+ "15-Oct-2020",
110
+ "22-Oct-2020",
111
+ "29-Oct-2020",
112
+ "05-Nov-2020",
113
+ "12-Nov-2020",
114
+ "19-Nov-2020",
115
+ "26-Nov-2020",
116
+ "03-Dec-2020",
117
+ "10-Dec-2020",
118
+ "17-Dec-2020",
119
+ "24-Dec-2020",
120
+ "31-Dec-2020"
121
+ ],
122
+ "2021": [
123
+ "07-Jan-2021",
124
+ "14-Jan-2021",
125
+ "21-Jan-2021",
126
+ "28-Jan-2021",
127
+ "04-Feb-2021",
128
+ "11-Feb-2021",
129
+ "18-Feb-2021",
130
+ "25-Feb-2021",
131
+ "04-Mar-2021",
132
+ "10-Mar-2021",
133
+ "18-Mar-2021",
134
+ "25-Mar-2021",
135
+ "01-Apr-2021",
136
+ "08-Apr-2021",
137
+ "15-Apr-2021",
138
+ "22-Apr-2021",
139
+ "29-Apr-2021",
140
+ "06-May-2021",
141
+ "12-May-2021",
142
+ "20-May-2021",
143
+ "27-May-2021",
144
+ "03-Jun-2021",
145
+ "10-Jun-2021",
146
+ "17-Jun-2021",
147
+ "24-Jun-2021",
148
+ "01-Jul-2021",
149
+ "08-Jul-2021",
150
+ "15-Jul-2021",
151
+ "22-Jul-2021",
152
+ "29-Jul-2021",
153
+ "05-Aug-2021",
154
+ "12-Aug-2021",
155
+ "18-Aug-2021",
156
+ "26-Aug-2021",
157
+ "02-Sep-2021",
158
+ "09-Sep-2021",
159
+ "16-Sep-2021",
160
+ "23-Sep-2021",
161
+ "30-Sep-2021",
162
+ "07-Oct-2021",
163
+ "14-Oct-2021",
164
+ "21-Oct-2021",
165
+ "28-Oct-2021",
166
+ "03-Nov-2021",
167
+ "11-Nov-2021",
168
+ "18-Nov-2021",
169
+ "25-Nov-2021",
170
+ "02-Dec-2021",
171
+ "09-Dec-2021",
172
+ "16-Dec-2021",
173
+ "23-Dec-2021",
174
+ "30-Dec-2021"
175
+ ],
176
+ "2022": ["06-Jan-2022",
177
+ "13-Jan-2022",
178
+ "20-Jan-2022",
179
+ "27-Jan-2022",
180
+ "03-Feb-2022",
181
+ "10-Feb-2022",
182
+ "17-Feb-2022",
183
+ "24-Feb-2022",
184
+ "03-Mar-2022",
185
+ "10-Mar-2022",
186
+ "17-Mar-2022",
187
+ "24-Mar-2022",
188
+ "31-Mar-2022",
189
+ "07-Apr-2022",
190
+ "13-Apr-2022",
191
+ "21-Apr-2022",
192
+ "28-Apr-2022",
193
+ "05-May-2022",
194
+ "12-May-2022",
195
+ "19-May-2022",
196
+ "26-May-2022",
197
+ "02-Jun-2022",
198
+ "09-Jun-2022",
199
+ "16-Jun-2022",
200
+ "23-Jun-2022",
201
+ "30-Jun-2022",
202
+ "07-Jul-2022",
203
+ "14-Jul-2022",
204
+ "21-Jul-2022",
205
+ "28-Jul-2022",
206
+ "04-Aug-2022",
207
+ "11-Aug-2022",
208
+ "18-Aug-2022",
209
+ "25-Aug-2022",
210
+ "01-Sep-2022",
211
+ "08-Sep-2022",
212
+ "15-Sep-2022",
213
+ "22-Sep-2022",
214
+ "29-Sep-2022",
215
+ "06-Oct-2022",
216
+ "13-Oct-2022",
217
+ "20-Oct-2022",
218
+ "27-Oct-2022",
219
+ "03-Nov-2022",
220
+ "10-Nov-2022",
221
+ "17-Nov-2022",
222
+ "24-Nov-2022",
223
+ "01-Dec-2022",
224
+ "08-Dec-2022",
225
+ "15-Dec-2022",
226
+ "22-Dec-2022",
227
+ "29-Dec-2022"],
228
+
229
+ }
230
+ }
pyarrow/connect.py ADDED
@@ -0,0 +1,399 @@
1
+ from datetime import datetime
2
+ import hashlib
3
+ import json
4
+ import logging
5
+ from typing import Any, Dict, List, Optional, Union
6
+ from urllib.parse import urlparse, parse_qs
7
+ import pyarrow.exceptions as ex
8
+ import pyotp
9
+ from pyarrow.arrow_utils import expiry_dates
10
+
11
+ from pyarrow.__version__ import __title__, __version__
12
+ from pyarrow.constants import Exchange, OrderType, ProductType, Retention, TransactionType, Variety
13
+
14
+ import dateutil.parser
15
+ import requests
16
+ import urllib3
17
+
18
+ import pyarrow.constants as constants
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+
23
+ class ArrowClient(object):
24
+ DEFAULT_TIMEOUT = 10
25
+ DEFAULT_ROOT_URL = "https://edge.arrow.trade"
26
+ DEFAULT_LOGIN_URL = "https://api.arrow.trade/auth/app/login"
27
+ VALIDATE_2FA_URL = "https://api.arrow.trade/auth/validate-2fa"
28
+
29
+ def __init__(
30
+ self, app_id: str,
31
+ timeout: Optional[int] = None,
32
+ debug: bool = False,
33
+ root_url: Optional[str] = None,
34
+ pool_config: Optional[Dict[str, Any]] = None) -> None:
35
+ self.app_id = app_id
36
+ self.req_session = requests.Session()
37
+ self.timeout = timeout or self.DEFAULT_TIMEOUT
38
+ self.debug = debug
39
+ self.root_url = root_url or self.DEFAULT_ROOT_URL
40
+ self.disable_ssl = False
41
+ self.proxies: Dict[str, str] = {}
42
+
43
+ self._token: Optional[str] = None
44
+ self._routes = constants.Routes(root_url=self.root_url)
45
+
46
+ def place_order(self,
47
+ exchange: Exchange,
48
+ symbol: str,
49
+ quantity: int,
50
+ disclosed_quantity: int,
51
+ product: ProductType,
52
+ order_type: OrderType,
53
+ variety: Variety,
54
+ transaction_type: TransactionType,
55
+ price: float,
56
+ validity: Retention,
57
+ tags: Optional[str] = None,
58
+ amo: bool = False,
59
+ trigger_price: Optional[float] = None) -> str:
60
+ params = dict(
61
+ exchange=str(exchange),
62
+ symbol=symbol,
63
+ # token=token,
64
+ quantity="{qty}".format(qty=quantity),
65
+ disclosedQty="{disclosed_qty}".format(disclosed_qty=disclosed_quantity),
66
+ product=str(product),
67
+ order=str(order_type),
68
+ transactionType=str(transaction_type),
69
+ price="{price}".format(price=price),
70
+ validity=str(validity),
71
+ tags=tags or "",
72
+ amo=amo,
73
+ triggerPrice="{trigger_price}".format(trigger_price=trigger_price or "")
74
+ ) # type: Dict[str, Any]
75
+ response = self._post(
76
+ self._routes.PLACE_ORDER,
77
+ url_args={"variety": str(variety)},
78
+ params=params
79
+ )
80
+ return response["orderNo"]
81
+
82
+ def set_token(self, token: str) -> None:
83
+ self._token = token
84
+
85
+ def get_token(self) -> str:
86
+ return self._token or ""
87
+
88
+ def invalidate_session(self) -> None:
89
+ self._token = None
90
+
91
+ # TODO
92
+ def get_instruments(self) -> Any:
93
+ # return self._get(self._routes.ALL_INSTRUMENTS)
94
+ pass
95
+
96
+ # Order Routes
97
+ def get_order(self, order_id: str) -> List[Dict[str, Any]]:
98
+ data = self._get(self._routes.GET_ORDER_BY_ID, url_args={"order_id": order_id})
99
+ return self._format_order_response(data)
100
+
101
+ def get_orders(self) -> List[Dict[str, Any]]:
102
+ """Get all user orders."""
103
+ data = self._get(self._routes.GET_USER_ORDERS)
104
+ return self._format_orders_response(data)
105
+
106
+ def get_trades(self) -> List[Dict[str, Any]]:
107
+ """Get all user trades."""
108
+ data = self._get(self._routes.GET_USER_TRADES)
109
+ return self._format_trades_response(data)
110
+
111
+ def modify_order(
112
+ self,
113
+ order_id: str,
114
+ exchange: Exchange,
115
+ quantity: int,
116
+ symbol: str,
117
+ price: float,
118
+ disclosed_qty: int,
119
+ product: ProductType,
120
+ transaction_type: TransactionType,
121
+ order_type: OrderType,
122
+ validity: Retention,
123
+ amo: bool = False,
124
+ trigger_price: Optional[float] = None,
125
+ tags: Optional[str] = None,
126
+ book_loss_price: Optional[float] = None,
127
+ book_profit_price: Optional[float] = None
128
+ ) -> str:
129
+ """Modify an existing order."""
130
+ params = {
131
+ "exchange": str(exchange),
132
+ "quantity": str(quantity),
133
+ "disclosedQty": str(disclosed_qty),
134
+ "product": str(product),
135
+ "symbol": symbol,
136
+ "transactionType": str(transaction_type),
137
+ "order": str(order_type),
138
+ "price": str(price),
139
+ "validity": str(validity),
140
+ "triggerPrice": str(trigger_price or "0"),
141
+ "bookLossPrice": str(book_loss_price or ""),
142
+ "bookProfitPrice": str(book_profit_price or ""),
143
+ "tags": tags or "",
144
+ "amo": amo
145
+ }
146
+
147
+ response = self._patch(
148
+ self._routes.UPDATE_ORDER_BY_ID,
149
+ url_args={"order_id": order_id},
150
+ params=params
151
+ )
152
+ return response["message"]
153
+
154
+ def cancel_order(self, order_id: str) -> str:
155
+ """Cancel an existing order."""
156
+ response = self._delete(
157
+ self._routes.DELETE_ORDER,
158
+ url_args={"order_id": order_id}
159
+ )
160
+ return response["message"]
161
+
162
+ # User Details Routes
163
+ def get_user_details(self) -> Dict[str, Any]:
164
+ return self._get(self._routes.USER_DETAILS)
165
+
166
+ # User Holdings
167
+ def get_holdings(self) -> Dict[str, Any]:
168
+ return self._get(self._routes.GET_USER_HOLDINGS)
169
+
170
+ @staticmethod
171
+ def get_expiry_dates(symbol: str, year: str):
172
+ return expiry_dates[symbol][year]
173
+
174
+ # Margin Routes
175
+ def get_order_margin(self, params: Dict[str, Any]) -> Dict[str, Any]:
176
+ return self._post(self._routes.ORDER_MARGIN, params=params)
177
+
178
+ def get_basket_margin(self, orders: List[Dict[str, Any]]) -> Dict[str, Any]:
179
+ return self._post(self._routes.BASKET_MARGIN, params=orders)
180
+
181
+ def _authenticate(self, checksum: str, request_token: str) -> Dict[str, str]:
182
+ params = {
183
+ "appID": self.app_id,
184
+ "token": request_token,
185
+ "checksum": checksum,
186
+ }
187
+ response = self._post("https://api.arrow.trade/auth/app/authenticate-token", params=params)
188
+ if "token" in response:
189
+ self._token = response["token"]
190
+ if self.debug:
191
+ log.debug(f"Token: {self._token}")
192
+
193
+ return response
194
+
195
+ def login_url(self) -> str:
196
+ return f"{self._routes.get_home_url()}?appID={self.app_id}"
197
+
198
+ def login(self, request_token: str, api_secret: str) -> Dict[str, str]:
199
+ checksum = self._generate_checksum(request_token, api_secret)
200
+ response = self._authenticate(checksum, request_token)
201
+ return response
202
+
203
+ def auto_login(self, user_id: str, password: str, api_secret: str, totp_secret: str) -> Dict[str, str]:
204
+ login_url = self.DEFAULT_LOGIN_URL
205
+ login_payload = {
206
+ "userID": user_id,
207
+ "password": password,
208
+ "captchaValue": "",
209
+ "captchaID": None,
210
+ "appID": self.app_id,
211
+ "isAppLogin": True
212
+ }
213
+
214
+ # Generate the request ID
215
+ try:
216
+ login_resp = self._post(login_url, params=login_payload)
217
+ request_id = login_resp["requestId"]
218
+ except Exception as e:
219
+ log.error("Failed to generate Request id: {}".format(e))
220
+ raise
221
+
222
+ # Generate the totp
223
+ try:
224
+ passcode = pyotp.TOTP(totp_secret).now()
225
+ except Exception as e:
226
+ log.error("Failed to Generate TOTP: {}".format(e))
227
+ raise
228
+
229
+ # Validate 2FA
230
+ totp_payload = {
231
+ "code": passcode, # Fixed: use passcode instead of request_id
232
+ "requestId": request_id,
233
+ "userID": user_id,
234
+ }
235
+
236
+ try:
237
+ totp_resp = self._post(self.VALIDATE_2FA_URL, params=totp_payload)
238
+ redirect_url = totp_resp["redirectUrl"]
239
+ except Exception as e:
240
+ log.error("Failed to Generate 2FA URL: {}".format(e))
241
+ raise
242
+
243
+ try:
244
+ parsed_url = urlparse(redirect_url)
245
+ request_token = parse_qs(parsed_url.query)["request-token"][0]
246
+ except Exception as e:
247
+ log.error("Failed to generate request token: {}".format(e))
248
+ raise
249
+
250
+ try:
251
+ response = self.login(request_token=request_token, api_secret=api_secret)
252
+ self.set_token(response["token"])
253
+ return response
254
+ except Exception as e:
255
+ log.error("Failed to complete login: {}".format(e))
256
+ raise
257
+
258
+ def _generate_checksum(self, request_token: str, api_secret: str) -> str:
259
+ input_str = f"{self.app_id}:{api_secret}:{request_token}"
260
+ return hashlib.sha256(input_str.encode("utf-8")).hexdigest()
261
+
262
+ # HTTP Route Methods
263
+ def _get(self, route: str, url_args: Optional[Dict[str, str]] = None, params: Any = None) -> Any:
264
+ return self._request("GET", route, url_args, params)
265
+
266
+ def _post(self, route: str, url_args: Optional[Dict[str, str]] = None, params: Any = None,
267
+ query_params: Any = None) -> Any:
268
+ return self._request("POST", route, url_args, params, query_params)
269
+
270
+ def _patch(self, route: str, url_args: Optional[Dict[str, str]] = None, params: Any = None,
271
+ query_params: Any = None) -> Any:
272
+ return self._request("PATCH", route, url_args, params, query_params)
273
+
274
+ def _delete(self, route: str, url_args: Optional[Dict[str, str]] = None, params: Any = None) -> Any:
275
+ return self._request("DELETE", route, url_args, params)
276
+
277
+ def _request(self, method: str, route: str,
278
+ url_args: Optional[Dict[str, str]] = None,
279
+ params: Any = None, query_params: Any = None) -> Any:
280
+ # Build URL
281
+ url = route.format(**url_args) if url_args else route
282
+
283
+ # Headers
284
+ headers = {
285
+ "User-Agent": f"pyarrow/{__version__}",
286
+ "appID": self.app_id,
287
+ "Content-Type": "application/json" # Add explicit content type
288
+ }
289
+
290
+ # Only add token header if we actually have a token (not empty string)
291
+ if self._token:
292
+ headers["token"] = self._token
293
+
294
+ if self.debug:
295
+ log.debug(f"Request: {method} {url} params={json.dumps(params)} headers={json.dumps(headers)}")
296
+
297
+ # Handle parameters correctly
298
+ json_data = None
299
+ query_params_final = query_params
300
+
301
+ if method in ["POST", "PUT", "PATCH"]:
302
+ json_data = params
303
+ elif method in ["GET", "DELETE"] and params:
304
+ query_params_final = params
305
+
306
+ try:
307
+ r = self.req_session.request(
308
+ method=method,
309
+ url=url,
310
+ json=json_data,
311
+ params=query_params_final,
312
+ headers=headers,
313
+ verify=not self.disable_ssl,
314
+ allow_redirects=True,
315
+ timeout=self.timeout,
316
+ proxies=self.proxies
317
+ )
318
+ except Exception as e:
319
+ raise e
320
+
321
+ if self.debug:
322
+ log.debug(f"Response: {method} {url}\n{r.content[:1000]!r}")
323
+
324
+ content_type = r.headers.get("content-type", "")
325
+
326
+ # JSON response
327
+ if "json" in content_type:
328
+ try:
329
+ data = r.json()
330
+ except ValueError:
331
+ raise ex.DataException(f"Invalid JSON response: {r.content!r}")
332
+
333
+ if isinstance(data, dict):
334
+ if data.get(constants.STATUS) == constants.ERROR or data.get(constants.ERROR_CODE):
335
+ if r.status_code == 403:
336
+ # TODO: handle session expiry
337
+ pass
338
+ exp = getattr(ex, str(data.get(constants.ERROR_CODE)), ex.GeneralException)
339
+ raise exp(data.get(constants.ERROR_MESSAGE, "Unknown Error"), code=r.status_code)
340
+ return data.get("data", data)
341
+ return data
342
+
343
+ # Binary response
344
+ if "octet-stream" in content_type:
345
+ return r.content
346
+
347
+ # Unknown response type
348
+ raise ex.DataException(f"Unknown Content-Type ({content_type}) Response: {r.content[:1000]!r}")
349
+
350
+ # Data Parsing Methods
351
+ def _format_order_response(self, data: Union[Dict[str, Any], List[Dict[str, Any]]]) -> List[Dict[str, Any]]:
352
+ """Format order response data."""
353
+ order_list = [data] if isinstance(data, dict) else data
354
+ return self._format_datetime_fields(order_list, ["timeStamp", "exchangeUpdateTime", "requestTime"])
355
+
356
+ def _format_orders_response(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
357
+ """Format orders response data."""
358
+ return self._format_datetime_fields(data, ["timeStamp", "exchangeUpdateTime"])
359
+
360
+ def _format_trades_response(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
361
+ """Format trades response data."""
362
+ return self._format_datetime_fields(data, ["fillTime", "timeStamp", "exchangeUpdateTime", "requestTime"])
363
+
364
+ @staticmethod
365
+ def _format_datetime_fields(data: List[Dict[str, Any]], datetime_fields: List[str]) -> List[Dict[str, Any]]:
366
+ """Convert datetime strings and epoch times in response data."""
367
+ for item in data:
368
+ # Convert datetime strings
369
+ for field in datetime_fields:
370
+ if item.get(field) and len(str(item[field])) == 19:
371
+ item[field] = dateutil.parser.parse(item[field])
372
+
373
+ # Convert epoch time to int
374
+ if item.get("orderTime"):
375
+ dt = datetime.strptime(item["orderTime"], "%Y-%m-%dT%H:%M:%S")
376
+ item["orderTime"] = int(dt.timestamp())
377
+
378
+ return data
379
+
380
+ def get_holidays(self) -> Dict[str, Any]:
381
+ return self._get(self._routes.HOLIDAYS_LIST)
382
+
383
+ def get_index_list(self) -> List[Dict[str, Any]]:
384
+ return self._get(self._routes.INDEX_LIST)
385
+
386
+ def get_option_chain_symbols(self):
387
+ return self._get(self._routes.OPTION_CHAIN_SYMBOLS)
388
+
389
+ def get_option_chain(self, params: Dict[str, str]) -> List[Dict[str, Any]]:
390
+ pass
391
+
392
+
393
+ # Properties
394
+ @property
395
+ def token(self) -> str:
396
+ return self._token or ""
397
+
398
+
399
+ Arrow = ArrowClient