degiro-cli 0.1.1__tar.gz → 0.1.3__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,19 +1,35 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: degiro-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Unofficial command line tools for Degiro
5
- Home-page: https://github.com/OhMajesticLama/degiro-cli
6
- Author-email: ohmajesticlama@gmail.com
5
+ Author-email: ohmajesticlama <ohmajesticlama@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/OhMajesticLama/degiro-cli
7
8
  Project-URL: Documentation, https://ohmajesticlama.github.io/degiro-cli
9
+ Keywords: degiro,cli,finance,investment
8
10
  Classifier: Programming Language :: Python :: 3
9
11
  Classifier: License :: OSI Approved :: MIT License
10
12
  Classifier: Operating System :: OS Independent
11
- Classifier: Development Status :: 4 - Beta
13
+ Classifier: Development Status :: 5 - Production/Stable
12
14
  Classifier: Intended Audience :: End Users/Desktop
13
15
  Classifier: Topic :: Office/Business :: Financial :: Investment
16
+ Requires-Python: >=3.8
14
17
  Description-Content-Type: text/markdown
15
- Provides-Extra: dev
16
18
  License-File: LICENSE
19
+ Requires-Dist: degiroasync>=1.0.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.0.1; extra == "dev"
22
+ Requires-Dist: coverage>=6.3; extra == "dev"
23
+ Requires-Dist: flake8>=4.0.1; extra == "dev"
24
+ Requires-Dist: mypy>=0.931; extra == "dev"
25
+ Requires-Dist: build>=0.7.0; extra == "dev"
26
+ Requires-Dist: twine>=3.8.0; extra == "dev"
27
+ Requires-Dist: sphinx>=4.4.0; extra == "dev"
28
+ Requires-Dist: sphinx_rtd_theme>=1.0.0; extra == "dev"
29
+ Requires-Dist: myst-parser>=0.17.0; extra == "dev"
30
+ Requires-Dist: ipython; extra == "dev"
31
+ Requires-Dist: ipdb; extra == "dev"
32
+ Dynamic: license-file
17
33
 
18
34
  # DegiroAsync Command Line Interface
19
35
 
@@ -1,19 +1,35 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: degiro-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Unofficial command line tools for Degiro
5
- Home-page: https://github.com/OhMajesticLama/degiro-cli
6
- Author-email: ohmajesticlama@gmail.com
5
+ Author-email: ohmajesticlama <ohmajesticlama@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/OhMajesticLama/degiro-cli
7
8
  Project-URL: Documentation, https://ohmajesticlama.github.io/degiro-cli
9
+ Keywords: degiro,cli,finance,investment
8
10
  Classifier: Programming Language :: Python :: 3
9
11
  Classifier: License :: OSI Approved :: MIT License
10
12
  Classifier: Operating System :: OS Independent
11
- Classifier: Development Status :: 4 - Beta
13
+ Classifier: Development Status :: 5 - Production/Stable
12
14
  Classifier: Intended Audience :: End Users/Desktop
13
15
  Classifier: Topic :: Office/Business :: Financial :: Investment
16
+ Requires-Python: >=3.8
14
17
  Description-Content-Type: text/markdown
15
- Provides-Extra: dev
16
18
  License-File: LICENSE
19
+ Requires-Dist: degiroasync>=1.0.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7.0.1; extra == "dev"
22
+ Requires-Dist: coverage>=6.3; extra == "dev"
23
+ Requires-Dist: flake8>=4.0.1; extra == "dev"
24
+ Requires-Dist: mypy>=0.931; extra == "dev"
25
+ Requires-Dist: build>=0.7.0; extra == "dev"
26
+ Requires-Dist: twine>=3.8.0; extra == "dev"
27
+ Requires-Dist: sphinx>=4.4.0; extra == "dev"
28
+ Requires-Dist: sphinx_rtd_theme>=1.0.0; extra == "dev"
29
+ Requires-Dist: myst-parser>=0.17.0; extra == "dev"
30
+ Requires-Dist: ipython; extra == "dev"
31
+ Requires-Dist: ipdb; extra == "dev"
32
+ Dynamic: license-file
17
33
 
18
34
  # DegiroAsync Command Line Interface
19
35
 
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ degiro_cli.egg-info/PKG-INFO
5
+ degiro_cli.egg-info/SOURCES.txt
6
+ degiro_cli.egg-info/dependency_links.txt
7
+ degiro_cli.egg-info/entry_points.txt
8
+ degiro_cli.egg-info/requires.txt
9
+ degiro_cli.egg-info/top_level.txt
10
+ degirocli/__init__.py
11
+ degirocli/helpers.py
12
+ degirocli/history.py
13
+ degirocli/login.py
14
+ degirocli/search.py
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ degiro-history = degirocli.history:main
3
+ degiro-login = degirocli.login:main
4
+ degiro-search = degirocli.search:main
@@ -0,0 +1,14 @@
1
+ degiroasync>=1.0.0
2
+
3
+ [dev]
4
+ pytest>=7.0.1
5
+ coverage>=6.3
6
+ flake8>=4.0.1
7
+ mypy>=0.931
8
+ build>=0.7.0
9
+ twine>=3.8.0
10
+ sphinx>=4.4.0
11
+ sphinx_rtd_theme>=1.0.0
12
+ myst-parser>=0.17.0
13
+ ipython
14
+ ipdb
@@ -0,0 +1 @@
1
+ degirocli
@@ -0,0 +1,61 @@
1
+ import enum
2
+ import logging
3
+ import asyncio
4
+ import collections
5
+ from typing import Callable
6
+ from typing import Iterable
7
+ from typing import Any
8
+ from typing import Sequence
9
+ from typing import Union
10
+
11
+
12
+ LOGGER = logging.getLogger('degirocli')
13
+
14
+ class ERROR_CODES(enum.IntEnum):
15
+ SESSION_EXISTS = 1
16
+
17
+ async def run_concurrent(
18
+ worker: Callable,
19
+ jobs: Iterable[Any],
20
+ *,
21
+ max_concurrents: int = 10,
22
+ ) -> Sequence[collections.abc.Awaitable]:
23
+ """
24
+ Run worker on jobs with a max of max_concurrents at the same time.
25
+
26
+ This is a helper to avoid sending too many queries at once to the producer
27
+ queried by worker.
28
+
29
+ >>> import asyncio
30
+ >>> async def worker(a, b):
31
+ ... await asyncio.sleep(1)
32
+ ... return a + b
33
+ ...
34
+ >>> awaitables = await run_concurrent(worker,
35
+ ... ((1, 2), (2, 3)))
36
+ ...
37
+ >>> res1 = await awaitables[0]
38
+ >>> res1
39
+ 3
40
+
41
+ Arguments
42
+ ---------
43
+
44
+ worker
45
+ Callable to apply to jobs.
46
+
47
+ jobs
48
+ Iterable of arguments that will be passed to worker.
49
+
50
+ max_concurrents
51
+ Maximum number of jobs that will be run concurrently.
52
+ """
53
+ LOGGER.debug('run_concurrent| worker %s', worker)
54
+ work_sem = asyncio.Semaphore(max_concurrents)
55
+
56
+ async def _wrapper(attrs):
57
+ async with work_sem:
58
+ res = await worker(*attrs)
59
+ return res
60
+
61
+ return [asyncio.create_task(_wrapper(attrs)) for attrs in jobs]
@@ -0,0 +1,200 @@
1
+ import logging
2
+ import argparse
3
+ import io
4
+ import sys
5
+ import asyncio
6
+ import csv
7
+ from typing import Tuple, Iterable
8
+ import getpass
9
+
10
+ import more_itertools
11
+ import degiroasync.api as dapi
12
+
13
+ from .helpers import run_concurrent
14
+ from .login import get_session_from_cache
15
+
16
+ LOGGER = logging.getLogger()
17
+
18
+
19
+ async def run_one(
20
+ session: dapi.Session,
21
+ exchange: str,
22
+ symbol: str,
23
+ period: dapi.PRICE.PERIOD
24
+ ):
25
+ """
26
+ """
27
+ try:
28
+ products = await dapi.search_product(
29
+ session,
30
+ by_symbol=symbol,
31
+ by_exchange=exchange,
32
+ product_type_id=dapi.PRODUCT.TYPEID.STOCK,
33
+ )
34
+ if len(products) > 1:
35
+ raise AssertionError("More than one product for {}.{}".format(
36
+ symbol,
37
+ exchange))
38
+ elif len(products) == 0:
39
+ LOGGER.error(
40
+ "No product found for exchange {}, symbol {}. Ignore.".format(
41
+ exchange, symbol
42
+ )
43
+ )
44
+ # Abort this one
45
+ return
46
+ product = products[0]
47
+ price_data: dapi.PriceSeriesTime = await dapi.get_price_data(
48
+ session,
49
+ product=product,
50
+ resolution=dapi.PRICE.RESOLUTION.PT1D,
51
+ period=period,
52
+ data_type=dapi.PRICE.TYPE.OHLC
53
+ )
54
+ except Exception as exc:
55
+ print(f"Error on symbol {exchange}.{symbol}: {exc}", file=sys.stderr)
56
+ return exc
57
+
58
+ writer = csv.writer(sys.stdout, delimiter=',')
59
+ writer.writerows(
60
+ (
61
+ exchange,
62
+ symbol,
63
+ date,
64
+ product.info.currency,
65
+ *price,
66
+ )
67
+
68
+ for date, price in zip(price_data.date, price_data.price)
69
+ )
70
+
71
+
72
+ def prepare_inputs(input_stream: Iterable[str]) -> Iterable[Tuple[str, str]]:
73
+ """
74
+ Filter & split inputs from input_stream. Ignore lines starting with #
75
+
76
+ Example
77
+ -------
78
+
79
+ >>> inputs = ['EPA.AIR', 'EPA.BNP', ' #EPA.VIE']
80
+ >>> list(prepare_inputs(inputs))
81
+ [('EPA', 'AIR'), ('EPA', 'BNP')]
82
+
83
+ """
84
+ # filter-out lines starting with #
85
+ inputs = (line.strip() for line in input_stream)
86
+ inputs = filter(lambda l: len(l) > 0, inputs)
87
+ inputs = filter(lambda l: l[0] != '#', inputs)
88
+ inputs = more_itertools.unique_everseen(inputs)
89
+ inputs = (line.split('.')[:2] for line in inputs)
90
+ return inputs # type: ignore , correct type.
91
+
92
+
93
+ async def run_price_pipeline(
94
+ session: dapi.Session,
95
+ *,
96
+ symbols: Iterable[str],
97
+ period: dapi.PRICE.PERIOD = dapi.PRICE.PERIOD.P1YEAR,
98
+ max_workers: int = 5
99
+ ):
100
+ """
101
+ Execute script: read symbols from standard input and output fetched price
102
+ data on standard output.
103
+ """
104
+ inputs = prepare_inputs(symbols)
105
+ inputs = ((session, *t, period) for t in inputs)
106
+ #inputs = ((session, t) for t in inputs)
107
+
108
+ queries = await run_concurrent(run_one, inputs, max_concurrents=max_workers)
109
+
110
+ # ensure all queries completed before quitting
111
+ return await asyncio.gather(*queries)
112
+
113
+
114
+ def main():
115
+ handler = logging.StreamHandler()
116
+
117
+ parser = argparse.ArgumentParser(
118
+ formatter_class=argparse.RawDescriptionHelpFormatter,
119
+ description="Get history for provided symbols stocks. Symbols are read "
120
+ "from standard input, and expected in format "
121
+ "EXCHANGE.SYMBOL "
122
+ "with EXCHANGE being product exchange on DEGIRO platform. "
123
+ )
124
+ parser.add_argument(
125
+ '-p', '--period',
126
+ default='1y',
127
+ dest='period',
128
+ help="Period on which to request data. Must be one of: "
129
+ "1d, 1m, 3m, 6m, 1y, 3y, 5y, 50y"
130
+ )
131
+ parser.add_argument(
132
+ '-H',
133
+ '--no-header-row',
134
+ dest='no_headers',
135
+ default=False,
136
+ action='store_true',
137
+ required=False,
138
+ help="Do not print header line"
139
+ )
140
+ parser.add_argument(
141
+ '--debug',
142
+ default=False,
143
+ action='store_true',
144
+ dest='debug',
145
+ help="Enable debug logging."
146
+ )
147
+ parser.add_argument(
148
+ 'symbols',
149
+ nargs='*',
150
+ default=[],
151
+ help="Symbols should be in the form of EXCHANGE.PRODUCT_SYMBOL"
152
+ )
153
+ args = parser.parse_args()
154
+ logging_level = logging.ERROR
155
+ if args.debug:
156
+ logging_level = logging.DEBUG
157
+ handler.setLevel(logging_level)
158
+ LOGGER.setLevel(logging_level)
159
+ LOGGER.addHandler(handler)
160
+
161
+ print_headers = not args.no_headers
162
+
163
+ period = {
164
+ '1d': dapi.PRICE.PERIOD.P1DAY,
165
+ '1m': dapi.PRICE.PERIOD.P1MONTH,
166
+ '3m': dapi.PRICE.PERIOD.P3MONTH,
167
+ '6m': dapi.PRICE.PERIOD.P6MONTH,
168
+ '1y': dapi.PRICE.PERIOD.P1YEAR,
169
+ '3y': dapi.PRICE.PERIOD.P3YEAR,
170
+ '5y': dapi.PRICE.PERIOD.P5YEAR,
171
+ '50y': dapi.PRICE.PERIOD.P50YEAR
172
+ }[args.period.lower()]
173
+
174
+ session = get_session_from_cache()
175
+ session.update_throttling(max_requests=7, period_seconds=1)
176
+
177
+ symbols = sys.stdin
178
+ if len(args.symbols) > 0:
179
+ symbols = args.symbols
180
+
181
+ if print_headers:
182
+ writer = csv.writer(sys.stdout)
183
+ writer.writerow((
184
+ "exchange",
185
+ "symbol",
186
+ "date",
187
+ "currency",
188
+ "open",
189
+ "high",
190
+ "low",
191
+ "close",
192
+ ))
193
+ asyncio.run(
194
+ run_price_pipeline(
195
+ session,
196
+ symbols=symbols,
197
+ period=period,
198
+ max_workers=5
199
+ )
200
+ )
@@ -0,0 +1,177 @@
1
+ import tempfile
2
+ import getpass
3
+ import functools
4
+ import multiprocessing
5
+ import stat
6
+ import asyncio
7
+ import json
8
+ import time
9
+ import pickle
10
+ import sys
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Union
14
+ from typing import Optional
15
+
16
+ import degiroasync.api as dapi
17
+ import degiroasync.webapi as wapi
18
+ import degiroasync.core as dcore
19
+
20
+ from .helpers import ERROR_CODES
21
+ from .helpers import LOGGER
22
+
23
+
24
+
25
+ def get_credentials() -> dapi.Credentials:
26
+ """
27
+ Helper to get credentials for degiroasync provider.
28
+ """
29
+ DEGIRO_USERNAME = 'DEGIRO_USERNAME'
30
+ DEGIRO_PASSWORD = 'DEGIRO_PASSWORD'
31
+ DEGIRO_TOTP_SECRET = 'DEGIRO_TOTP_SECRET'
32
+ if (username := os.environ.get(DEGIRO_USERNAME)) is None:
33
+ username = input('Username: ')
34
+ if len(username.strip()) == 0:
35
+ raise AssertionError(
36
+ "{} not set and not provided".format(DEGIRO_USERNAME))
37
+ if (password := os.environ.get(DEGIRO_PASSWORD)) is None:
38
+ password = getpass.getpass('Password: ')
39
+ if len(password.strip()) == 0:
40
+ raise AssertionError("{} not set and not provided".format(DEGIRO_PASSWORD))
41
+ totp_secret: Optional[str] = os.environ.get(DEGIRO_TOTP_SECRET) or None # could be ''
42
+ totp = None
43
+ if totp_secret is None:
44
+ # Ask user for TOTP
45
+ totp: Optional[str] = input("One Time Password (Enter to ignore): ")
46
+ if len(totp.strip()) == 0:
47
+ totp = None
48
+
49
+ return dapi.Credentials(
50
+ username=username,
51
+ password=password,
52
+ totp_secret=totp_secret,
53
+ one_time_password=totp
54
+ )
55
+
56
+
57
+ def get_tmp_path():
58
+ tmpdir = Path(tempfile.gettempdir())
59
+ return tmpdir / 'degirocli'
60
+
61
+ def _get_hash(path: Union[Path, str]) -> str:
62
+ with open(path, 'rb') as fh:
63
+ fhash = hash(fh)
64
+ return fhash
65
+
66
+
67
+ def expire_path(
68
+ path: Union[Path, str],
69
+ lifetime_seconds: Union[int, float]=60*60*2,
70
+ start_time_seconds: Optional[Union[int, float]] = None,
71
+ force: bool = False,
72
+ ):
73
+ """
74
+ Will remove file at path after 'lifetime_seconds'.
75
+
76
+ Parameters
77
+ ----------
78
+
79
+ path
80
+ Path to remove when expired. By default, `path` will only be removed
81
+ if it hasn't changed since watcher started.
82
+
83
+ lifetime_seconds
84
+ File at path will be expired at start_time_seconds + lifetime_seconds.
85
+
86
+ start_time_seconds
87
+ File at path will be expired at start_time_seconds + lifetime_seconds.
88
+ Defaults to time.time().
89
+
90
+ force
91
+ If true, file at `path` will be removed even if it has changed since
92
+ this function has been called.
93
+ """
94
+ # Let's detach this process.
95
+ if os.fork() != 0: # If os.fork == 0 this is the detached process.
96
+ return
97
+ if not isinstance(path, Path):
98
+ assert isinstance(path, str)
99
+ path = Path(path)
100
+ fhash = _get_hash(path)
101
+ start_time_seconds = start_time_seconds or time.time()
102
+
103
+ while time.time() - start_time_seconds < lifetime_seconds:
104
+ time.sleep(time.time() - start_time_seconds)
105
+
106
+ tmp_path = get_tmp_path()
107
+ if tmp_path.exists():
108
+ if tmp_path.is_file():
109
+ fhash_new = _get_hash(path)
110
+ if fhash_new == fhash or force:
111
+ Path.unlink(tmp_path)
112
+ else:
113
+ print(f'{path} has changed since it has been set to expire. '
114
+ f'Abort deletion.', file=sys.stderr)
115
+ return 1
116
+ return 0
117
+
118
+
119
+ #@functools.wraps(_expire_path)
120
+ #def expire_path(*args):
121
+ # # Start event loop to
122
+ # proc = multiprocessing.Process(
123
+ # target=_expire_path,
124
+ # args=args,
125
+ # daemon=False)
126
+ # proc.start()
127
+
128
+
129
+ async def login():
130
+ tmp_path = get_tmp_path()
131
+
132
+ if tmp_path.exists():
133
+ ans = input("Existing session found, delete? (y/N)").strip().lower()
134
+ if ans in ('y', 'yes'):
135
+ tmp_path.unlink()
136
+ else:
137
+ print('Abort.')
138
+ return ERROR_CODES.SESSION_EXISTS
139
+
140
+ # Get sessionID and write it
141
+ credentials = get_credentials()
142
+ session = await wapi.login(credentials)
143
+ tmp_path.touch()
144
+ tmp_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
145
+ with open(tmp_path, 'w') as fh:
146
+ json.dump({
147
+ 'version': 1,
148
+ 'format': 'degirocli',
149
+ 'session': dict(
150
+ cookies=session.cookies,
151
+ ),
152
+ }, fh)
153
+ # Delete file in 3 hour
154
+ expire_path(tmp_path, 3*60*60)
155
+
156
+ async def _get_session_from_cache() -> dapi.Session:
157
+ cache_path = get_tmp_path()
158
+ if not cache_path.exists():
159
+ raise AssertionError(
160
+ f"{cache_path} not found. Abort. Have you tried degiro-login?")
161
+ with open(cache_path, 'r') as fh:
162
+ session_d = json.load(fh)
163
+ session = dcore.SessionCore()
164
+ session._cookies = session_d['session']['cookies']
165
+ await wapi.get_config(session)
166
+ await wapi.get_client_info(session)
167
+ exc_dict = await dapi.get_dictionary(session)
168
+ return dapi.Session(session, exc_dict)
169
+
170
+ def get_session_from_cache() -> dapi.Session:
171
+ """
172
+ Helper to get Session when already logged in.
173
+ """
174
+ return asyncio.run(_get_session_from_cache())
175
+
176
+ def main():
177
+ asyncio.run(login())
@@ -0,0 +1,174 @@
1
+ import sys
2
+ import asyncio
3
+ import logging
4
+ import argparse
5
+ import csv
6
+ from typing import Optional
7
+
8
+ import degiroasync.api as dapi
9
+
10
+ from .helpers import LOGGER
11
+ from .login import get_session_from_cache
12
+
13
+
14
+ async def run_search_pipeline(
15
+ session: dapi.Session,
16
+ *,
17
+ search_txt: Optional[str],
18
+ exchange_txt: Optional[str],
19
+ country_txt: Optional[str],
20
+ index_txt: Optional[str]
21
+ ):
22
+ """
23
+ Execute script: read symbols from standard input and output fetched price
24
+ data on standard output.
25
+ """
26
+ exc_dict = session.dictionary
27
+ products = await dapi.search_product(
28
+ session=session,
29
+ by_text=search_txt,
30
+ by_exchange=exchange_txt,
31
+ by_country=country_txt,
32
+ by_index=index_txt,
33
+ )
34
+ writer = csv.writer(sys.stdout)
35
+ exc_dict: dapi.ExchangeDictionary = session.dictionary
36
+ for product in products:
37
+ exchange = exc_dict.exchange_by(id=product.info.exchange_id)
38
+ writer.writerow((
39
+ exchange.hiq_abbr,
40
+ product.info.symbol,
41
+ product.info.name,
42
+ product.info.currency,
43
+ product.info.isin,
44
+ ))
45
+
46
+
47
+
48
+ def main():
49
+ handler = logging.StreamHandler()
50
+
51
+ parser = argparse.ArgumentParser(
52
+ formatter_class=argparse.RawDescriptionHelpFormatter,
53
+ description="Get history for provided symbols stocks. Symbols are read "
54
+ "from standard input, and expected in format "
55
+ "EXCHANGE.SYMBOL "
56
+ "with EXCHANGE being product exchange on DEGIRO platform. "
57
+ )
58
+ parser.add_argument(
59
+ '-t',
60
+ dest='search',
61
+ default=None,
62
+ required=False,
63
+ help="Search for SEARCH_TXT and returns products."
64
+ )
65
+ parser.add_argument(
66
+ '--exchange',
67
+ dest='exchange',
68
+ default=None,
69
+ required=False,
70
+ help="Search for products by exchange."
71
+ )
72
+ parser.add_argument(
73
+ '--country',
74
+ dest='country',
75
+ default=None,
76
+ required=False,
77
+ help="Search products on exchange places in COUNTRY"
78
+ )
79
+ parser.add_argument(
80
+ '--list-countries',
81
+ dest='list_countries',
82
+ default=False,
83
+ action='store_true',
84
+ required=False,
85
+ help="Print the available country codes on the platform."
86
+ )
87
+ parser.add_argument(
88
+ '--index',
89
+ dest='index',
90
+ default=None,
91
+ required=False,
92
+ help="Search products on exchange places in INDEX"
93
+ )
94
+ parser.add_argument(
95
+ '--list-indices',
96
+ dest='list_indices',
97
+ default=False,
98
+ action='store_true',
99
+ required=False,
100
+ help="Print the available incides codes on the platform."
101
+ )
102
+ parser.add_argument(
103
+ '-H',
104
+ '--no-header-row',
105
+ dest='no_headers',
106
+ default=False,
107
+ action='store_true',
108
+ required=False,
109
+ help="Do not print header line"
110
+ )
111
+ parser.add_argument(
112
+ '--debug',
113
+ default=False,
114
+ action='store_true',
115
+ dest='debug',
116
+ help="Enable debug logging."
117
+ )
118
+ args = parser.parse_args()
119
+
120
+ print_headers = not args.no_headers
121
+ search_txt = args.search
122
+ exchange_txt = args.exchange
123
+ country_txt = args.country
124
+ index_txt = args.index
125
+ list_indices = args.list_indices
126
+
127
+ logging_level = logging.ERROR
128
+ if args.debug:
129
+ logging_level = logging.DEBUG
130
+ handler.setLevel(logging_level)
131
+ LOGGER.setLevel(logging_level)
132
+ LOGGER.addHandler(handler)
133
+
134
+ session = get_session_from_cache()
135
+ # Lower throttling limit here as there is a good chance we'll
136
+ # bigger period limits: it seems degiro sends browser checks
137
+ # after many requests have been issued.
138
+ session.update_throttling(max_requests=10, period_seconds=1)
139
+
140
+ if args.list_countries:
141
+ for country in sorted(
142
+ session.dictionary.countries,
143
+ key=lambda x: x.name):
144
+ print(country.name)
145
+ return 0
146
+ if args.list_indices:
147
+ for index in sorted(
148
+ session.dictionary.indices,
149
+ key=lambda x: x.name):
150
+ if index.product_id is not None:
151
+ # 202307
152
+ # There is currently no support to pull products from an index
153
+ # where product_id not set by the platform, so don't show them.
154
+ print(index.name)
155
+ return 0
156
+
157
+ if print_headers:
158
+ writer = csv.writer(sys.stdout)
159
+ writer.writerow((
160
+ "exchange",
161
+ "symbol",
162
+ "name",
163
+ "currency",
164
+ "isin",
165
+ ))
166
+ asyncio.run(
167
+ run_search_pipeline(
168
+ session,
169
+ search_txt=search_txt,
170
+ exchange_txt=exchange_txt,
171
+ country_txt=country_txt,
172
+ index_txt=index_txt
173
+ )
174
+ )
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "degiro-cli"
7
+ version = "0.1.3"
8
+ authors = [{ name = "ohmajesticlama", email = "ohmajesticlama@gmail.com" }]
9
+ description = "Unofficial command line tools for Degiro"
10
+ readme = "README.md"
11
+ requires-python = ">=3.8"
12
+ license = { text = "MIT" }
13
+
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Development Status :: 5 - Production/Stable",
19
+ "Intended Audience :: End Users/Desktop",
20
+ "Topic :: Office/Business :: Financial :: Investment"
21
+ ]
22
+
23
+ keywords = ["degiro", "cli", "finance", "investment"]
24
+
25
+ dependencies = [
26
+ "degiroasync >= 1.0.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ "Homepage" = "https://github.com/OhMajesticLama/degiro-cli"
31
+ "Documentation" = "https://ohmajesticlama.github.io/degiro-cli"
32
+
33
+ [project.scripts]
34
+ degiro-login = "degirocli.login:main"
35
+ degiro-history = "degirocli.history:main"
36
+ degiro-search = "degirocli.search:main"
37
+
38
+ [project.optional-dependencies]
39
+ dev = [
40
+ # Tests
41
+ "pytest >= 7.0.1",
42
+ "coverage >= 6.3",
43
+ # Code quality
44
+ "flake8 >= 4.0.1",
45
+ "mypy >= 0.931",
46
+ # For shipping
47
+ "build >= 0.7.0",
48
+ "twine >= 3.8.0",
49
+ # Documentation
50
+ "sphinx >= 4.4.0",
51
+ "sphinx_rtd_theme >= 1.0.0",
52
+ "myst-parser >= 0.17.0",
53
+ # Other dev tools
54
+ "ipython",
55
+ "ipdb",
56
+ ]
57
+
58
+ [tool.setuptools]
59
+ packages = ["degirocli"]
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
-
4
- if __name__ == '__main__':
5
- import sys
6
- import degirocli.history
7
- res = degirocli.history.run_cli()
8
- sys.exit(res)
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
-
4
- if __name__ == '__main__':
5
- import sys
6
- import degirocli.login
7
- res = degirocli.login.run_cli()
8
- sys.exit(res)
File without changes
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
-
4
- if __name__ == '__main__':
5
- import sys
6
- import degirocli.search
7
- res = degirocli.search.run_cli()
8
- sys.exit(res)
@@ -1,14 +0,0 @@
1
- LICENSE
2
- README.md
3
- pyproject.toml
4
- setup.py
5
- bin/degiro-history
6
- bin/degiro-login
7
- bin/degiro-logout
8
- bin/degiro-portfolio
9
- bin/degiro-search
10
- degiro_cli.egg-info/PKG-INFO
11
- degiro_cli.egg-info/SOURCES.txt
12
- degiro_cli.egg-info/dependency_links.txt
13
- degiro_cli.egg-info/requires.txt
14
- degiro_cli.egg-info/top_level.txt
@@ -1,9 +0,0 @@
1
- degiroasync>=0.20.0
2
-
3
- [dev]
4
- pytest>=7.0.1
5
- coverage>=6.3
6
- flake8>=4.0.1
7
- mypy>=0.931
8
- ipython
9
- ipdb
@@ -1,4 +0,0 @@
1
- [build-system]
2
- requires=["setuptools >= 59.6.0", "wheel >= 0.34.2"]
3
- build-backend= "setuptools.build_meta"
4
-
degiro-cli-0.1.1/setup.py DELETED
@@ -1,67 +0,0 @@
1
- """
2
- setup file for degiroasync
3
- """
4
- import sys
5
- import os
6
-
7
- import setuptools
8
-
9
-
10
- if __name__ == '__main__':
11
- description = "Unofficial command line tools for Degiro"
12
- readme_path = os.path.join(os.path.dirname(__file__), 'README.md')
13
- with open(readme_path, "r") as fh:
14
- long_description = fh.read()
15
-
16
- setuptools.setup(
17
- name="degiro-cli",
18
- version="0.1.1",
19
- author_email="ohmajesticlama@gmail.com",
20
- description=description,
21
- long_description=long_description,
22
- long_description_content_type='text/markdown',
23
- url="https://github.com/OhMajesticLama/degiro-cli",
24
- project_urls={
25
- 'Documentation':
26
- 'https://ohmajesticlama.github.io/degiro-cli'
27
- },
28
- packages=setuptools.find_packages(),
29
- scripts=[
30
- os.path.join('bin', 'degiro-login'),
31
- os.path.join('bin', 'degiro-history'),
32
- os.path.join('bin', 'degiro-search'),
33
- ],
34
- install_requires=[
35
- 'degiroasync >= 0.20.0',
36
- ],
37
- extras_require={
38
- 'dev': [
39
- # Tests
40
- 'pytest >= 7.0.1',
41
- 'coverage >= 6.3',
42
- # Code quality
43
- 'flake8 >= 4.0.1',
44
- 'mypy >= 0.931',
45
- # For shipping
46
- #'build >= 0.7.0',
47
- #'twine >= 3.8.0',
48
- # Documentation
49
- #'sphinx >= 4.4.0',
50
- #'sphinx_rtd_theme >= 1.0.0',
51
- #'myst-parser >= 0.17.0', # markdown imports
52
- # Other dev tools
53
- 'ipython',
54
- 'ipdb',
55
- ]
56
- },
57
- classifiers=[
58
- "Programming Language :: Python :: 3",
59
- "License :: OSI Approved :: MIT License",
60
- "Operating System :: OS Independent",
61
- "Development Status :: 4 - Beta",
62
- "Intended Audience :: End Users/Desktop",
63
- "Topic :: Office/Business :: Financial :: Investment"
64
- ],
65
- test_suite='pytest',
66
- tests_require=['pytest']
67
- )
File without changes
File without changes
File without changes