p123api 2.4.2__tar.gz → 3.0.0a2__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.
@@ -1,18 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: p123api
3
- Version: 2.4.2
3
+ Version: 3.0.0a2
4
4
  Summary: Portfolio123 API wrapper
5
- Home-page: https://github.com/portfolio-123/p123api-py
6
- Author: Portfolio123
7
- Author-email: info@portfolio123.com
8
- License: UNKNOWN
9
- Platform: UNKNOWN
5
+ Author-email: Portfolio123 <info@portfolio123.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/portfolio-123/p123api-py
10
8
  Classifier: Programming Language :: Python :: 3
11
- Classifier: License :: OSI Approved :: MIT License
12
9
  Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.6
10
+ Requires-Python: >=3.10
14
11
  Description-Content-Type: text/markdown
15
12
  License-File: LICENSE
13
+ Requires-Dist: requests
14
+ Requires-Dist: typing_extensions
15
+ Provides-Extra: pandas
16
+ Requires-Dist: pandas; extra == "pandas"
17
+ Dynamic: license-file
16
18
 
17
19
  # Portfolio123 API Wrapper
18
20
 
@@ -25,5 +27,3 @@ with p123api.Client(api_id='your api id', api_key='your api key') as client:
25
27
  except p123api.ClientException as e:
26
28
  print(e)
27
29
  ```
28
-
29
-
@@ -0,0 +1,2 @@
1
+ from p123api.client import Client, ClientException, ClientItemNotFoundException
2
+ from p123api.types import IdResult, DataSeriesInfoResult, DataSeriesResult, StockFactorResult, StockFactorInfoResult, StrategyInfoResult
@@ -1,15 +1,11 @@
1
- from collections import defaultdict
1
+ from collections.abc import Callable
2
2
  import requests
3
3
  import time
4
- import pandas
5
4
  from string import Template
6
- from typing import IO, Callable, List, Literal, Optional, Union, overload
5
+ from typing import IO, Any, Literal, overload
7
6
  from typing_extensions import deprecated
8
- import numpy
9
7
 
10
- numpy.any(32)
11
-
12
- from .types import (
8
+ from p123api.types import (
13
9
  DataSeriesInfoResult,
14
10
  DataSeriesResult,
15
11
  IdResult,
@@ -59,16 +55,15 @@ AIFACTOR_PREDICT_PATH = Template("/aiFactor/predict/$id")
59
55
 
60
56
 
61
57
  class ClientException(Exception):
62
- def __init__(self, message, *, resp: Union[requests.Response, None] = None, exception: Union[Exception, None] = None):
58
+ def __init__(self, message, *, resp: requests.Response | None = None):
63
59
  super().__init__(message)
64
60
  self._resp = resp
65
- self._exception = exception
66
61
 
67
62
  def get_resp(self):
68
63
  return self._resp
69
64
 
70
65
  def get_cause(self):
71
- return self._exception
66
+ return self.__cause__
72
67
 
73
68
  @staticmethod
74
69
  def build(message, resp: requests.Response):
@@ -87,19 +82,19 @@ class Client:
87
82
  class for interfacing with P123 API
88
83
  """
89
84
 
90
- def __init__(self, *, api_id, api_key, auth_extra={}, endpoint=ENDPOINT, verify_requests=True):
85
+ def __init__(self, *, api_id: str | int, api_key: str, auth_extra={}, endpoint=ENDPOINT, verify_requests=True):
91
86
  self._endpoint = endpoint
92
87
  self._verify_requests = verify_requests
93
88
  self._max_req_retries = 5
94
89
  self._timeout = 300
95
90
  self._token = None
96
91
 
97
- if not isinstance(api_id, str) or not api_id:
98
- raise ClientException("api_id needs to be a non-empty str")
99
- if not isinstance(api_key, str) or not api_key:
100
- raise ClientException("api_key needs to be a non-empty str")
92
+ if not isinstance(api_id, (str, int)):
93
+ raise ClientException("api_id must be str or int")
94
+ if not isinstance(api_key, str):
95
+ raise ClientException("api_key must be str")
101
96
 
102
- self._auth_params = {"apiId": api_id, "apiKey": api_key, **auth_extra}
97
+ self._auth_params = {"apiId": str(api_id), "apiKey": api_key, **auth_extra}
103
98
  self._session = requests.Session()
104
99
  self._method_map = {"GET": self._session.get, "POST": self._session.post, "DELETE": self._session.delete}
105
100
 
@@ -112,12 +107,12 @@ class Client:
112
107
  def close(self):
113
108
  self._session.close()
114
109
 
115
- def set_max_request_retries(self, retries):
110
+ def set_max_request_retries(self, retries: int):
116
111
  if not isinstance(retries, int) or retries < 1 or retries > 10:
117
112
  raise ClientException("retries needs to be an int between 1 and 10")
118
113
  self._max_req_retries = retries
119
114
 
120
- def set_timeout(self, timeout):
115
+ def set_timeout(self, timeout: int):
121
116
  if not isinstance(timeout, int) or timeout < 1:
122
117
  raise ClientException("timeout needs to be an int greater than 0")
123
118
  self._timeout = timeout
@@ -161,7 +156,15 @@ class Client:
161
156
  raise ClientException(f"API authentication failed{message}", resp=resp)
162
157
 
163
158
  def _req_with_auth_fallback(
164
- self, *, method: Literal["GET", "POST", "DELETE"] = "POST", url: str, json=None, params=None, data=None, headers=None
159
+ self,
160
+ *,
161
+ method: Literal["GET", "POST", "DELETE"] = "POST",
162
+ url: str,
163
+ json: Any = None,
164
+ params: list[tuple[str, Any]] | None = None,
165
+ data: str | IO | None = None,
166
+ headers: dict[str, str] | None = None,
167
+ result_type: type | None = None,
165
168
  ):
166
169
  """
167
170
  Request with authentication fallback, used by all requests (except authentication)
@@ -190,7 +193,8 @@ class Client:
190
193
  ) as resp:
191
194
 
192
195
  if resp.status_code == 200:
193
- return resp.json()
196
+ json = resp.json()
197
+ return result_type(json) if result_type is not None else json
194
198
 
195
199
  if resp.status_code == 401 or resp.status_code == 403:
196
200
  del self._session.headers["Authorization"]
@@ -215,6 +219,8 @@ class Client:
215
219
  ret = self._req_with_auth_fallback(url=self._endpoint + SCREEN_ROLLING_BACKTEST_PATH, json=params)
216
220
 
217
221
  if to_pandas:
222
+ import pandas
223
+
218
224
  rows = ret["rows"]
219
225
  ret["average"][0] = "Average"
220
226
  rows.append(ret["average"])
@@ -236,6 +242,8 @@ class Client:
236
242
  ret = self._req_with_auth_fallback(url=self._endpoint + SCREEN_BACKTEST_PATH, json=params)
237
243
 
238
244
  if to_pandas:
245
+ import pandas
246
+
239
247
  columns = [
240
248
  "",
241
249
  "Total Return",
@@ -314,6 +322,8 @@ class Client:
314
322
  ret = self._req_with_auth_fallback(url=self._endpoint + SCREEN_RUN_PATH, json=params)
315
323
 
316
324
  if to_pandas:
325
+ import pandas
326
+
317
327
  ret = pandas.DataFrame(data=ret["rows"], columns=ret["columns"])
318
328
 
319
329
  return ret
@@ -344,6 +354,8 @@ class Client:
344
354
  ret = self._req_with_auth_fallback(url=self._endpoint + DATA_PATH, json=params)
345
355
 
346
356
  if to_pandas:
357
+ import pandas
358
+
347
359
  raw_obj = dict(ret)
348
360
  with_cusips = params.get("cusips") is not None
349
361
  with_name = params.get("includeNames")
@@ -380,28 +392,38 @@ class Client:
380
392
  ret = self._req_with_auth_fallback(url=self._endpoint + DATA_UNIVERSE_PATH, json=params)
381
393
 
382
394
  if to_pandas:
395
+ import pandas
396
+
383
397
  raw_obj = ret
384
- names = params.get("names")
385
398
  f_indices = range(len(params["formulas"]))
399
+
400
+ names = params.get("names")
401
+ if names is None:
402
+ names = [f"formula{i + 1}" for i in f_indices]
403
+
386
404
  if params.get("asOfDt"):
387
405
  for formula_idx in f_indices:
388
- name = names[formula_idx] if names is not None else f"formula{formula_idx + 1}"
406
+ name = names[formula_idx]
389
407
  ret[name] = ret["data"][formula_idx]
390
408
  del ret["dt"], ret["cost"], ret["quotaRemaining"], ret["data"]
391
409
  ret = pandas.DataFrame(ret)
392
410
  else:
411
+ includeNames = bool(params.get("includeNames"))
412
+ includeFigi = bool(params.get("figi"))
413
+
393
414
  data = {"dates": [], "p123Uids": [], "tickers": []}
394
- includeNames = False
395
- if params.get("includeNames"):
415
+ if includeNames:
396
416
  data["names"] = []
397
- includeNames = True
398
- includeFigi = False
399
- if params.get("figi"):
417
+ if includeFigi:
400
418
  data["figi"] = []
401
- includeFigi = True
402
- formulas = defaultdict(list)
419
+
420
+ date_data = [[] for _ in f_indices]
421
+
422
+ for formula_idx in f_indices:
423
+ data[names[formula_idx]] = []
424
+
403
425
  for dtObj in ret["dates"]:
404
- data["dates"].extend(dtObj["dt"] for _ in range(len(dtObj["p123Uids"])))
426
+ data["dates"].extend([dtObj["dt"]] * len(dtObj["p123Uids"]))
405
427
  data["p123Uids"].extend(dtObj["p123Uids"])
406
428
  data["tickers"].extend(dtObj["tickers"])
407
429
  if includeNames:
@@ -409,10 +431,7 @@ class Client:
409
431
  if includeFigi:
410
432
  data["figi"].extend(dtObj["figi"])
411
433
  for formula_idx in f_indices:
412
- formulas[formula_idx].extend(dtObj["data"][formula_idx])
413
- for formula_idx in f_indices:
414
- name = names[formula_idx] if names is not None else f"formula{formula_idx + 1}"
415
- data[name] = formulas[formula_idx]
434
+ date_data[formula_idx].extend(dtObj["data"][formula_idx])
416
435
  ret = pandas.DataFrame(data)
417
436
  ret.attrs["raw_obj"] = raw_obj
418
437
 
@@ -428,6 +447,8 @@ class Client:
428
447
  ret = self._req_with_auth_fallback(url=self._endpoint + RANK_RANKS_PATH, json=params)
429
448
 
430
449
  if to_pandas:
450
+ import pandas
451
+
431
452
  names = dict()
432
453
  raw_obj = dict(ret)
433
454
  del ret["cost"], ret["quotaRemaining"], ret["dt"]
@@ -577,8 +598,10 @@ class Client:
577
598
  """
578
599
  ...
579
600
 
580
- def rank_get(self, *, id: Optional[int] = None, name: Optional[str] = None) -> RankInfoResult:
581
- return self._req_with_auth_fallback(method="GET", url=self._endpoint + RANK_PATH, params={"id": id, "name": name})
601
+ def rank_get(self, *, id: int | None = None, name: str | None = None) -> RankInfoResult:
602
+ return self._req_with_auth_fallback(
603
+ method="GET", url=self._endpoint + RANK_PATH, params=[("id", id), ("name", name)], result_type=RankInfoResult
604
+ )
582
605
 
583
606
  def strategy(self, strategy_id: int):
584
607
  """
@@ -612,7 +635,10 @@ class Client:
612
635
  }
613
636
  """
614
637
  return self._req_with_auth_fallback(
615
- method="POST", url=self._endpoint + STRATEGY_COPY_PATH.substitute(id=id), json={"name": name, "type": type}
638
+ method="POST",
639
+ url=self._endpoint + STRATEGY_COPY_PATH.substitute(id=id),
640
+ json={"name": name, "type": type},
641
+ result_type=IdResult,
616
642
  )
617
643
 
618
644
  def book_copy(self, id: int, name: str, type: Literal["BOOK", "BOOKSIM"]) -> IdResult:
@@ -638,7 +664,7 @@ class Client:
638
664
  }
639
665
  """
640
666
  return self._req_with_auth_fallback(
641
- method="POST", url=self._endpoint + BOOK_COPY_PATH.substitute(id=id), json={"name": name, "type": type}
667
+ method="POST", url=self._endpoint + BOOK_COPY_PATH.substitute(id=id), json={"name": name, "type": type}, result_type=IdResult
642
668
  )
643
669
 
644
670
  def strategy_transactions(self, strategy_id: int, start: str, end: str, to_pandas=False):
@@ -653,12 +679,18 @@ class Client:
653
679
  ret = self._req_with_auth_fallback(
654
680
  method="GET", url=self._endpoint + STRATEGY_TRANS_PATH.substitute(id=strategy_id), params=[("start", start), ("end", end)]
655
681
  )
656
- return pandas.DataFrame(ret["trans"]) if to_pandas else ret
682
+
683
+ if to_pandas:
684
+ import pandas
685
+
686
+ return pandas.DataFrame(ret["trans"])
687
+
688
+ return ret
657
689
 
658
690
  def strategy_transaction_import(
659
691
  self,
660
692
  strategy_id: int,
661
- data: Union[str, IO[str]],
693
+ data: str | IO[str],
662
694
  content_type: Literal["text/csv", "text/tsv"] = "text/csv",
663
695
  update_existing=False,
664
696
  make_rebal_dt_curr=False,
@@ -686,7 +718,7 @@ class Client:
686
718
  headers={"Content-Type": content_type},
687
719
  )
688
720
 
689
- def strategy_transaction_delete(self, strategy_id: int, params: List[int]):
721
+ def strategy_transaction_delete(self, strategy_id: int, params: list[int]):
690
722
  """
691
723
  Strategy transaction delete
692
724
  :param strategy_id:
@@ -697,7 +729,7 @@ class Client:
697
729
  method="DELETE", url=self._endpoint + STRATEGY_TRANS_PATH.substitute(id=strategy_id), json=params
698
730
  )
699
731
 
700
- def strategy_holdings(self, strategy_id: int, date: Optional[str] = None, to_pandas=False):
732
+ def strategy_holdings(self, strategy_id: int, date: str | None = None, to_pandas=False):
701
733
  """
702
734
  Strategy holdings
703
735
  :param strategy_id:
@@ -711,7 +743,12 @@ class Client:
711
743
  method="GET", url=self._endpoint + STRATEGY_HOLDINGS_PATH.substitute(id=strategy_id), params=get_params
712
744
  )
713
745
 
714
- return pandas.DataFrame(ret["holdings"]) if to_pandas else ret
746
+ if to_pandas:
747
+ import pandas
748
+
749
+ return pandas.DataFrame(ret["holdings"])
750
+
751
+ return ret
715
752
 
716
753
  def strategy_trading_system(self, strategy_id: int):
717
754
  """
@@ -789,39 +826,70 @@ class Client:
789
826
  def stock_factor_upload(
790
827
  self,
791
828
  factor_id: int,
792
- data: Union[str, IO[str]],
793
- column_separator: Union[str, None] = None,
794
- existing_data: Union[str, None] = None,
795
- date_format: Union[str, None] = None,
796
- decimal_separator: Union[str, None] = None,
797
- ignore_errors: Union[bool, None] = None,
798
- ignore_duplicates: Union[bool, None] = None,
829
+ data: str | IO[str],
830
+ column_separator: Literal[",", ";", "\t"] = ",",
831
+ existing_data: Literal["overwrite", "skip", "delete"] = "overwrite",
832
+ date_format="yyyy-mm-dd",
833
+ decimal_separator: Literal[".", ","] = ".",
834
+ ignore_errors=False,
835
+ ignore_duplicates=False,
799
836
  ):
800
837
  """
801
- Stock factor data upload
802
- :param factor_id:
803
- :param data:
804
- :param column_separator: comma, semicolon or tab
805
- :param existing_data: overwrite, skip or delete
806
- :param date_format: dd for day, mm for month and yyyy for year, any separator allowed (defaults to yyyy-mm-dd)
807
- :param decimal_separator: . or ,
808
- :param ignore_errors:
809
- :param ignore_duplicates:
810
- :return:
811
- """
812
- get_params = []
813
- if column_separator is not None:
814
- get_params.append(("columnSeparator", column_separator))
815
- if existing_data is not None:
816
- get_params.append(("existingData", existing_data))
817
- if date_format is not None:
818
- get_params.append(("dateFormat", date_format))
819
- if decimal_separator is not None:
820
- get_params.append(("decimalSeparator", decimal_separator))
821
- if ignore_errors is not None:
822
- get_params.append(("onError", "continue" if ignore_errors else "stop"))
823
- if ignore_duplicates is not None:
824
- get_params.append(("onDuplicates", "continue" if ignore_duplicates else "stop"))
838
+ Upload stock factor data.
839
+
840
+ Uploads delimited content to the specified stock factor.
841
+
842
+ The uploaded content must contain a header row.
843
+ Only the first three columns are processed and must contain ``date``, ``<identifier>``, and ``value``.
844
+
845
+ ``<identifier>`` may be one of ``id`` (Portfolio123 stock ID), ``ticker`` (Portfolio123 ticker), ``gvkey``, ``cik``, or ``figi``.
846
+
847
+ The ``value`` column may specify ``na``, ``nan``, or ``null`` (case-insensitive) to clear a prior value on an observation date.
848
+
849
+ Example input::
850
+
851
+ date,ticker,value
852
+ 2026-01-31,AAPL:USA,1.25
853
+ 2026-01-31,MSFT:USA,0.83
854
+ 2026-02-28,MSFT:USA,na
855
+
856
+ Note that some types of identifiers may resolve to multiple stocks.
857
+ For example, the FIGI ``BBG001SG1LP6`` resolves to both ``UMC:USA`` and ``UMCB:DEU``.
858
+
859
+ Identifier resolution is performed at the time of the upload. If identifier relationships ever change or coverage expands,
860
+ the stock factor data will still reflect the original resolution.
861
+
862
+ Args:
863
+ factor_id: Unique identifier of the stock factor.
864
+ data: Delimited content string or file-like containing delimited content. Must not exceed 100 MB or 5 million lines.
865
+ column_separator: Separator character between columns. Defaults to comma.
866
+ existing_data: Policy for dealing with collisions against stored (date, stock ID) pairs. Defaults to ``overwrite``.
867
+ - ``overwrite``: Overwrite stored values.
868
+ - ``skip``: Retaine stored values.
869
+ - ``delete``: Clear before storing uploaded data.
870
+ date_format: Date format. Defaults to ``yyyy-mm-dd``.
871
+ decimal_separator: Decimal separator. Defaults to period. If comma is used, the thousands separator, if used, is assumed to be period.
872
+ ignore_errors: If ``True``, lines in the data with errors will be silently discarded.
873
+ ignore_duplicates: If ``True``, additional occurrences of a (date, identifier) pair in the data are skipped.
874
+ """
875
+
876
+ # COMPAT: column_separator originally accepted 'comma', 'semicolon', 'tab' which matches the API.
877
+ actual_column_separator: str = column_separator
878
+ if column_separator == ",":
879
+ actual_column_separator = "comma"
880
+ elif column_separator == ";":
881
+ actual_column_separator = "semicolon"
882
+ elif column_separator == "\t":
883
+ actual_column_separator = "tab"
884
+
885
+ get_params = [
886
+ ("columnSeparator", actual_column_separator),
887
+ ("existingData", existing_data),
888
+ ("dateFormat", date_format),
889
+ ("decimalSeparator", decimal_separator),
890
+ ("onError", "continue" if ignore_errors else "stop"),
891
+ ("onDuplicates", "continue" if ignore_duplicates else "stop"),
892
+ ]
825
893
  return self._req_with_auth_fallback(
826
894
  url=self._endpoint + STOCK_FACTOR_UPLOAD_PATH.substitute(id=factor_id), params=get_params, data=data
827
895
  )
@@ -832,7 +900,9 @@ class Client:
832
900
  :param params:
833
901
  :return:
834
902
  """
835
- return self._req_with_auth_fallback(url=self._endpoint + STOCK_FACTOR_CREATE_UPDATE_PATH, json=params)
903
+ return self._req_with_auth_fallback(
904
+ url=self._endpoint + STOCK_FACTOR_CREATE_UPDATE_PATH, json=params, result_type=StockFactorResult
905
+ )
836
906
 
837
907
  def stock_factor_delete(self, factor_id: int):
838
908
  """
@@ -845,39 +915,51 @@ class Client:
845
915
  def data_series_upload(
846
916
  self,
847
917
  series_id: int,
848
- data: Union[str, IO[str]],
849
- existing_data: Union[str, None] = None,
850
- date_format: Union[str, None] = None,
851
- decimal_separator: Union[str, None] = None,
852
- ignore_errors: Union[bool, None] = None,
853
- ignore_duplicates: Union[bool, None] = None,
854
- contains_header_row: Union[bool, None] = None,
918
+ data: str | IO[str],
919
+ existing_data: Literal["overwrite", "skip", "delete"] = "overwrite",
920
+ date_format="yyyy-mm-dd",
921
+ decimal_separator: Literal[".", ","] = ".",
922
+ ignore_errors=False,
923
+ ignore_duplicates=False,
924
+ contains_header_row=True,
855
925
  ):
856
926
  """
857
- Data series upload
858
- :param series_id:
859
- :param data:
860
- :param existing_data: overwrite, skip or delete
861
- :param date_format: dd for day, mm for month and yyyy for year, any separator allowed (defaults to yyyy-mm-dd)
862
- :param decimal_separator: . or ,
863
- :param ignore_errors:
864
- :param ignore_duplicates:
865
- :param contains_header_row:
866
- :return:
867
- """
868
- get_params = []
869
- if existing_data is not None:
870
- get_params.append(("existingData", existing_data))
871
- if date_format is not None:
872
- get_params.append(("dateFormat", date_format))
873
- if decimal_separator is not None:
874
- get_params.append(("decimalSeparator", decimal_separator))
875
- if ignore_errors is not None:
876
- get_params.append(("onError", "continue" if ignore_errors else "stop"))
877
- if ignore_duplicates is not None:
878
- get_params.append(("onDuplicates", "continue" if ignore_duplicates else "stop"))
879
- if contains_header_row is not None:
880
- get_params.append(("headerRow", contains_header_row))
927
+ Upload data series data.
928
+
929
+ Uploads delimited content to the specified data series.
930
+
931
+ The data must contain dates in the first column and values in the second column.
932
+ If the data includes a header row, the names are not processed.
933
+
934
+ The ``value`` column may specify ``na``, ``nan``, or ``null`` (case-insensitive) to clear a prior value on an observation date.
935
+
936
+ Example input::
937
+
938
+ date,value
939
+ 2026-01-31,1.25
940
+ 2026-02-28,na
941
+
942
+ Args:
943
+ series_id: Unique identifier of the data series.
944
+ data: Delimited content string or file-like containing delimited content. Must not exceed 100 MB.
945
+ existing_data: Policy for dealing with collisions against stored dates. Defaults to ``overwrite``.
946
+ - ``overwrite``: Overwrite stored values.
947
+ - ``skip``: Retaine stored values.
948
+ - ``delete``: Clear before storing uploaded data.
949
+ date_format: Date format. Defaults to ``yyyy-mm-dd``.
950
+ decimal_separator: Decimal separator. Defaults to period. If comma is used, the thousands separator, if used, is assumed to be period.
951
+ ignore_errors: If ``True``, lines in the data with errors will be silently discarded.
952
+ ignore_duplicates: If ``True``, additional occurrences of a date in the data are skipped.
953
+ contains_header_row: If ``True``, the first line of the uploaded data will be skipped.
954
+ """
955
+ get_params = [
956
+ ("existingData", existing_data),
957
+ ("dateFormat", date_format),
958
+ ("decimalSeparator", decimal_separator),
959
+ ("onError", "continue" if ignore_errors else "stop"),
960
+ ("onDuplicates", "continue" if ignore_duplicates else "stop"),
961
+ ("headerRow", contains_header_row),
962
+ ]
881
963
  return self._req_with_auth_fallback(
882
964
  url=self._endpoint + DATA_SERIES_UPLOAD_PATH.substitute(id=series_id), params=get_params, data=data
883
965
  )
@@ -888,7 +970,7 @@ class Client:
888
970
  :param params:
889
971
  :return:
890
972
  """
891
- return self._req_with_auth_fallback(url=self._endpoint + DATA_SERIES_CREATE_UPDATE_PATH, json=params)
973
+ return self._req_with_auth_fallback(url=self._endpoint + DATA_SERIES_CREATE_UPDATE_PATH, json=params, result_type=DataSeriesResult)
892
974
 
893
975
  def data_series_delete(self, series_id: int):
894
976
  """
@@ -911,6 +993,8 @@ class Client:
911
993
  ret = self._req_with_auth_fallback(url=self._endpoint + AIFACTOR_PREDICT_PATH.substitute(id=predictor_id), json=params)
912
994
 
913
995
  if to_pandas:
996
+ import pandas
997
+
914
998
  data = {"p123Uid": ret["p123Uids"], "ticker": ret["tickers"]}
915
999
  if "names" in ret:
916
1000
  data["name"] = ret["names"]
@@ -939,7 +1023,7 @@ class Client:
939
1023
  """
940
1024
  return self._req_with_auth_fallback(method="GET", url=self._endpoint + STOCK_FACTOR_DOWNLOAD_PATH.substitute(id=factor_id))
941
1025
 
942
- def data_prices(self, identifier: Union[int, str], start: str, end: Optional[str], to_pandas=False):
1026
+ def data_prices(self, identifier: int | str, start: str, end: str | None, to_pandas=False):
943
1027
  """ """
944
1028
  get_params = [("start", start)]
945
1029
  if end is not None:
@@ -947,7 +1031,13 @@ class Client:
947
1031
  ret = self._req_with_auth_fallback(
948
1032
  method="GET", url=self._endpoint + DATA_PRICES_PATH.substitute(identifier=identifier), params=get_params
949
1033
  )
950
- return pandas.DataFrame(ret["prices"]) if to_pandas else ret
1034
+
1035
+ if to_pandas:
1036
+ import pandas
1037
+
1038
+ return pandas.DataFrame(ret["prices"])
1039
+
1040
+ return ret
951
1041
 
952
1042
  @overload
953
1043
  def stock_factor_info(self, *, id: int) -> StockFactorInfoResult:
@@ -963,9 +1053,9 @@ class Client:
963
1053
  Examples:
964
1054
  >>> client.stock_factor_info(id=123)
965
1055
  {
966
- factorId: 123,
967
- name: 'Stock factor name',
968
- description: 'Stock factor description'
1056
+ 'factorId': 123,
1057
+ 'name': 'Stock factor name',
1058
+ 'description': 'Stock factor description'
969
1059
  }
970
1060
  """
971
1061
  ...
@@ -984,9 +1074,9 @@ class Client:
984
1074
  Examples:
985
1075
  >>> client.stock_factor_info(name='Stock factor name')
986
1076
  {
987
- factorId: 123,
988
- name: 'Stock factor name',
989
- description: 'Stock factor description'
1077
+ 'factorId': 123,
1078
+ 'name': 'Stock factor name',
1079
+ 'description': 'Stock factor description'
990
1080
  }
991
1081
  """
992
1082
  ...
@@ -1009,23 +1099,23 @@ class Client:
1009
1099
  Examples:
1010
1100
  >>> client.stock_factor_info(factor_id=123)
1011
1101
  {
1012
- factorId: 123,
1013
- name: 'Stock factor name',
1014
- description: 'Stock factor description'
1102
+ 'factorId': 123,
1103
+ 'name': 'Stock factor name',
1104
+ 'description': 'Stock factor description'
1015
1105
  }
1016
1106
  """
1017
1107
  ...
1018
1108
 
1019
- def stock_factor_info(
1020
- self, *, id: Optional[int] = None, factor_id: Optional[int] = None, name: Optional[str] = None
1021
- ) -> StockFactorInfoResult:
1109
+ def stock_factor_info(self, *, id: int | None = None, factor_id: int | None = None, name: str | None = None) -> StockFactorInfoResult:
1022
1110
  if id is not None:
1023
- params = {"id": id}
1111
+ params = [("id", id)]
1024
1112
  elif factor_id is not None:
1025
- params = {"id": factor_id}
1113
+ params = [("id", factor_id)]
1026
1114
  else:
1027
- params = {"name": name}
1028
- return self._req_with_auth_fallback(method="GET", url=self._endpoint + STOCK_FACTOR_INFO_PATH, params=params)
1115
+ params = [("name", name)]
1116
+ return self._req_with_auth_fallback(
1117
+ method="GET", url=self._endpoint + STOCK_FACTOR_INFO_PATH, params=params, result_type=StockFactorInfoResult
1118
+ )
1029
1119
 
1030
1120
  @overload
1031
1121
  def data_series_info(self, *, id: int) -> DataSeriesInfoResult:
@@ -1041,9 +1131,9 @@ class Client:
1041
1131
  Examples:
1042
1132
  >>> client.data_series_info(id=123)
1043
1133
  {
1044
- dataSeriesId: 123,
1045
- name: 'Data series name',
1046
- description: 'Data series description'
1134
+ 'dataSeriesId': 123,
1135
+ 'name': 'Data series name',
1136
+ 'description': 'Data series description'
1047
1137
  }
1048
1138
  """
1049
1139
  ...
@@ -1062,16 +1152,19 @@ class Client:
1062
1152
  Examples:
1063
1153
  >>> client.data_series_info(name='Data series name')
1064
1154
  {
1065
- dataSeriesId: 123,
1066
- name: 'Data series name',
1067
- description: 'Data series description'
1155
+ 'dataSeriesId': 123,
1156
+ 'name': 'Data series name',
1157
+ 'description': 'Data series description'
1068
1158
  }
1069
1159
  """
1070
1160
  ...
1071
1161
 
1072
- def data_series_info(self, *, id: Optional[int] = None, name: Optional[str] = None) -> DataSeriesInfoResult:
1162
+ def data_series_info(self, *, id: int | None = None, name: str | None = None) -> DataSeriesInfoResult:
1073
1163
  return self._req_with_auth_fallback(
1074
- method="GET", url=self._endpoint + DATA_SERIES_INFO_PATH, params={"name": name} if id is None else {"id": id}
1164
+ method="GET",
1165
+ url=self._endpoint + DATA_SERIES_INFO_PATH,
1166
+ params=[("name", name)] if id is None else [("id", id)],
1167
+ result_type=DataSeriesInfoResult,
1075
1168
  )
1076
1169
 
1077
1170
  @overload
@@ -1088,9 +1181,9 @@ class Client:
1088
1181
  Examples:
1089
1182
  >>> client.strategy_info(id=123)
1090
1183
  {
1091
- strategyId: 123,
1092
- name: 'Strategy name',
1093
- description: 'Strategy description'
1184
+ 'strategyId': 123,
1185
+ 'name': 'Strategy name',
1186
+ 'description': 'Strategy description'
1094
1187
  }
1095
1188
  """
1096
1189
  ...
@@ -1109,16 +1202,19 @@ class Client:
1109
1202
  Examples:
1110
1203
  >>> client.strategy_info(name='Strategy name')
1111
1204
  {
1112
- strategyId: 123,
1113
- name: 'Strategy name',
1114
- description: 'Strategy description'
1205
+ 'strategyId': 123,
1206
+ 'name': 'Strategy name',
1207
+ 'description': 'Strategy description'
1115
1208
  }
1116
1209
  """
1117
1210
  ...
1118
1211
 
1119
- def strategy_info(self, *, id: Optional[int] = None, name: Optional[str] = None) -> StrategyInfoResult:
1212
+ def strategy_info(self, *, id: int | None = None, name: str | None = None) -> StrategyInfoResult:
1120
1213
  return self._req_with_auth_fallback(
1121
- method="GET", url=self._endpoint + STRATEGY_INFO_PATH, params={"name": name} if id is None else {"id": id}
1214
+ method="GET",
1215
+ url=self._endpoint + STRATEGY_INFO_PATH,
1216
+ params=[("name", name)] if id is None else [("id", id)],
1217
+ result_type=StrategyInfoResult,
1122
1218
  )
1123
1219
 
1124
1220
 
@@ -1140,4 +1236,4 @@ def req_with_retry(req: Callable[..., requests.Response], max_tries=5, **kwargs)
1140
1236
  tries += 1
1141
1237
  if tries >= max_tries:
1142
1238
  break
1143
- raise ClientException("Cannot connect to API", exception=exception)
1239
+ raise ClientException("Cannot connect to API") from exception
@@ -0,0 +1,110 @@
1
+ from enum import Enum, IntEnum
2
+ import inspect
3
+ import typing
4
+ from typing import Literal
5
+
6
+
7
+ def _create_fn(name, args, body, globals_dict=None):
8
+ """Executes a string of code to build a highly optimized function."""
9
+ args_str = ", ".join(args)
10
+ body_str = "\n ".join(body)
11
+
12
+ txt = f"""
13
+ def __builder__():
14
+ def {name}({args_str}):
15
+ {body_str}
16
+ return {name}
17
+ """
18
+ namespace = {}
19
+ exec(txt, globals_dict or {}, namespace)
20
+ return namespace["__builder__"]()
21
+
22
+
23
+ def _slow_init(self, d: dict):
24
+ cls = type(self)
25
+ annotations = typing.get_type_hints(cls)
26
+
27
+ globals_dict = {}
28
+ for hint in annotations.values():
29
+ if inspect.isclass(hint) and issubclass(hint, Enum):
30
+ globals_dict[hint.__name__] = hint
31
+
32
+ init_args = ["self", "d"]
33
+ init_body = []
34
+
35
+ for key, expected_type in annotations.items():
36
+ if inspect.isclass(expected_type) and issubclass(expected_type, Enum):
37
+ init_body.append(f"self.{key} = {expected_type.__name__}(v) if (v := d.get({key!r})) is not None else None")
38
+ else:
39
+ init_body.append(f"self.{key} = d.get({key!r})")
40
+
41
+ init = cls.__init__ = _create_fn("__init__", init_args, init_body, globals_dict)
42
+ init(self, d)
43
+
44
+
45
+ def _slow_repr(self):
46
+ cls = type(self)
47
+ annotations = typing.get_type_hints(cls)
48
+
49
+ repr = cls.__repr__ = _create_fn(
50
+ "__repr__", ["self"], [f"return f'{cls.__name__}({', '.join(f'{k}={{self.{k}!r}}' for k in annotations)})'"]
51
+ )
52
+ return repr(self)
53
+
54
+
55
+ def api_result(cls):
56
+ cls.__init__ = _slow_init
57
+ cls.__repr__ = _slow_repr
58
+ return cls
59
+
60
+
61
+ @api_result
62
+ class IdResult:
63
+ id: int
64
+
65
+
66
+ @api_result
67
+ class DataSeriesResult:
68
+ dataSeriesId: int
69
+
70
+
71
+ @api_result
72
+ class DataSeriesInfoResult:
73
+ dataSeriesId: int
74
+ name: str
75
+
76
+
77
+ @api_result
78
+ class StockFactorResult:
79
+ factorId: int
80
+
81
+
82
+ @api_result
83
+ class StockFactorInfoResult:
84
+ factorId: int
85
+ name: str
86
+
87
+
88
+ class RankingMethod(IntEnum):
89
+ PERCENTILE_NA_NEGATIVE = 2
90
+ PERCENTILE_NA_NEUTRAL = 4
91
+ NORMAL_DISTRIBUTION = 1
92
+
93
+
94
+ @api_result
95
+ class RankInfoResult:
96
+ name: str
97
+ id: int
98
+ xml: str
99
+ currency: str
100
+ rankingMethod: RankingMethod
101
+ type: Literal["Stock", "ETF"]
102
+ description: str | None
103
+ groupUid: int
104
+ resolveGroupUid: int
105
+
106
+
107
+ @api_result
108
+ class StrategyInfoResult:
109
+ strategyId: int
110
+ name: str
@@ -1,18 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: p123api
3
- Version: 2.4.2
3
+ Version: 3.0.0a2
4
4
  Summary: Portfolio123 API wrapper
5
- Home-page: https://github.com/portfolio-123/p123api-py
6
- Author: Portfolio123
7
- Author-email: info@portfolio123.com
8
- License: UNKNOWN
9
- Platform: UNKNOWN
5
+ Author-email: Portfolio123 <info@portfolio123.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/portfolio-123/p123api-py
10
8
  Classifier: Programming Language :: Python :: 3
11
- Classifier: License :: OSI Approved :: MIT License
12
9
  Classifier: Operating System :: OS Independent
13
- Requires-Python: >=3.6
10
+ Requires-Python: >=3.10
14
11
  Description-Content-Type: text/markdown
15
12
  License-File: LICENSE
13
+ Requires-Dist: requests
14
+ Requires-Dist: typing_extensions
15
+ Provides-Extra: pandas
16
+ Requires-Dist: pandas; extra == "pandas"
17
+ Dynamic: license-file
16
18
 
17
19
  # Portfolio123 API Wrapper
18
20
 
@@ -25,5 +27,3 @@ with p123api.Client(api_id='your api id', api_key='your api key') as client:
25
27
  except p123api.ClientException as e:
26
28
  print(e)
27
29
  ```
28
-
29
-
@@ -1,7 +1,6 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
- setup.py
5
4
  p123api/__init__.py
6
5
  p123api/client.py
7
6
  p123api/types.py
@@ -0,0 +1,5 @@
1
+ requests
2
+ typing_extensions
3
+
4
+ [pandas]
5
+ pandas
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "p123api"
7
+ version = "3.0.0a2"
8
+ description = "Portfolio123 API wrapper"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ {name = "Portfolio123", email = "info@portfolio123.com"}
14
+ ]
15
+
16
+ dependencies = [
17
+ "requests",
18
+ "typing_extensions",
19
+ ]
20
+
21
+ classifiers = [
22
+ "Programming Language :: Python :: 3",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ pandas = [
28
+ "pandas",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/portfolio-123/p123api-py"
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = ["p123api*"]
36
+
37
+ [tool.black]
38
+ line-length = 140
39
+ skip-magic-trailing-comma = true
40
+
41
+ [tool.pyright]
42
+ pythonVersion = "3.10"
43
+ typeCheckingMode = "basic"
44
+ deprecateTypingAliases = true
45
+ disableBytesTypePromotions = true
46
+ strictDictionaryInference = true
47
+ strictListInference = true
48
+ strictSetInference = true
@@ -1 +0,0 @@
1
- from .client import Client, ClientException, ClientItemNotFoundException
@@ -1,53 +0,0 @@
1
- from enum import IntEnum
2
- from typing import Literal, Optional, TypedDict
3
-
4
-
5
- class SharedResult(TypedDict):
6
- cost: int
7
- quotaRemaining: str
8
-
9
-
10
- class IdResult(SharedResult):
11
- id: int
12
-
13
-
14
- class DataSeriesResult(SharedResult):
15
- dataSeriesId: int
16
-
17
-
18
- class DataSeriesInfoResult(DataSeriesResult):
19
- name: str
20
- description: str
21
-
22
-
23
- class StockFactorResult(SharedResult):
24
- factorId: int
25
-
26
-
27
- class StockFactorInfoResult(StockFactorResult):
28
- name: str
29
- description: str
30
-
31
-
32
- class RankInfoResult(SharedResult):
33
- name: str
34
- id: int
35
- xml: str
36
- currency: str
37
- rankingMethod: int
38
- type: Literal["Stock", "ETF"]
39
- description: Optional[str]
40
- groupUid: int
41
- resolveGroupUid: int
42
-
43
-
44
- class RankingMethod(IntEnum):
45
- PERCENTILE_NA_NEGATIVE = 2
46
- PERCENTILE_NA_NEUTRAL = 4
47
- NORMAL_DISTRIBUTION = 1
48
-
49
-
50
- class StrategyInfoResult(SharedResult):
51
- strategyId: int
52
- name: str
53
- description: str
@@ -1,2 +0,0 @@
1
- pandas
2
- requests
@@ -1,12 +0,0 @@
1
- [tool.black]
2
- line-length = 140
3
- skip-magic-trailing-comma = true
4
-
5
- [tool.pyright]
6
- pythonVersion = "3.6"
7
- typeCheckingMode = "basic"
8
- deprecateTypingAliases = true
9
- disableBytesTypePromotions = true
10
- strictDictionaryInference = true
11
- strictListInference = true
12
- strictSetInference = true
p123api-2.4.2/setup.py DELETED
@@ -1,19 +0,0 @@
1
- import setuptools
2
-
3
- with open("README.md", "r") as fh:
4
- long_description = fh.read()
5
-
6
- setuptools.setup(
7
- name="p123api",
8
- version="2.4.2",
9
- author="Portfolio123",
10
- author_email="info@portfolio123.com",
11
- description="Portfolio123 API wrapper",
12
- long_description=long_description,
13
- long_description_content_type="text/markdown",
14
- url="https://github.com/portfolio-123/p123api-py",
15
- packages=setuptools.find_packages(),
16
- classifiers=["Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent"],
17
- python_requires=">=3.6",
18
- install_requires=["pandas", "requests"],
19
- )
File without changes
File without changes
File without changes