seolpyo-mplchart 1.4.0.3__py3-none-any.whl → 1.4.0.4__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.

Potentially problematic release.


This version of seolpyo-mplchart might be problematic. Click here for more details.

@@ -321,10 +321,10 @@ class BoxMixin(CrossLineMixin):
321
321
  else:
322
322
  # 캔들 강조
323
323
  self.in_candle = True
324
- x1, x2 = (self.intx-0.3, self.intx+1.4)
324
+ x1, x2 = (self.intx-0.3, self.intx+1.3)
325
325
  self.collection_price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
326
326
  self.collection_price_box.draw(renderer)
327
- else:
327
+ elif self.volume:
328
328
  # 거래량 강조
329
329
  high = self.df['max_box_volume'][self.intx]
330
330
  low = 0
@@ -333,7 +333,7 @@ class BoxMixin(CrossLineMixin):
333
333
  if high < y or y < low: self.in_volumebar = False
334
334
  else:
335
335
  self.in_volumebar = True
336
- x1, x2 = (self.intx-0.3, self.intx+1.4)
336
+ x1, x2 = (self.intx-0.3, self.intx+1.3)
337
337
  self.collection_volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
338
338
  self.collection_volume_box.draw(renderer)
339
339
  return
@@ -399,7 +399,7 @@ class InfoMixin(BoxMixin):
399
399
 
400
400
  if self.intx is not None:
401
401
  if self.in_candle: self._draw_candle_info_artist(e)
402
- elif self.in_volumebar: self._draw_volume_info_artist(e)
402
+ elif self.volume and self.in_volumebar: self._draw_volume_info_artist(e)
403
403
  return
404
404
 
405
405
  def _draw_candle_info_artist(self, e: MouseEvent):
@@ -437,6 +437,18 @@ class InfoMixin(BoxMixin):
437
437
  self.artist_text_volume_info.draw(self.figure.canvas.renderer)
438
438
  return
439
439
 
440
+ def get_info_kwargs(self, is_price: bool, **kwargs)-> dict:
441
+ """
442
+ get text info kwargs
443
+
444
+ Args:
445
+ is_price (bool): is price chart info or not
446
+
447
+ Returns:
448
+ dict[str, any]: text info kwargs
449
+ """
450
+ return kwargs
451
+
440
452
  def _get_info(self, index, is_price=True):
441
453
  dt = self.df[self.date][index]
442
454
  if not self.volume: v, vr = ('-', '-')
@@ -474,7 +486,8 @@ class InfoMixin(BoxMixin):
474
486
  if ld[1]: l = f'{float_to_str(ld[0])} {Fraction(ld[1])}'
475
487
  else: l = float_to_str(ld[0])
476
488
 
477
- text = self.format_candleinfo.format(
489
+ kwargs = self.get_info_kwargs(
490
+ is_price=is_price,
478
491
  dt=dt,
479
492
  close=f'{c:>{self._length_text}}{self.unit_price}',
480
493
  rate=f'{r:>{self._length_text}}%',
@@ -484,11 +497,13 @@ class InfoMixin(BoxMixin):
484
497
  low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
485
498
  volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=f'{vr}%',
486
499
  )
500
+ text = self.format_candleinfo.format(**kwargs)
487
501
  else:
488
502
  o, h, l, c = (float_to_str(o, self.digit_price), float_to_str(h, self.digit_price), float_to_str(l, self.digit_price), float_to_str(c, self.digit_price))
489
503
  com = float_to_str(compare, self.digit_price, plus=True)
490
504
 
491
- text = self.format_candleinfo.format(
505
+ kwargs = self.get_info_kwargs(
506
+ is_price=is_price,
492
507
  dt=dt,
493
508
  close=f'{c:>{self._length_text}}{self.unit_price}',
494
509
  rate=f'{r:>{self._length_text}}%',
@@ -498,15 +513,18 @@ class InfoMixin(BoxMixin):
498
513
  low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
499
514
  volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=f'{vr}%',
500
515
  )
516
+ text = self.format_candleinfo.format(**kwargs)
501
517
  elif self.volume:
502
518
  compare = self.df['compare_volume'][index]
503
519
  com = float_to_str(compare, self.digit_volume, plus=True)
504
- text = self.format_volumeinfo.format(
520
+ kwargs = self.get_info_kwargs(
521
+ is_price=is_price,
505
522
  dt=dt,
506
523
  volume=f'{v:>{self._length_text}}{self.unit_volume}',
507
524
  rate_volume=f'{vr:>{self._length_text}}%',
508
525
  compare=f'{com:>{self._length_text}}{self.unit_volume}',
509
526
  )
527
+ text = self.format_volumeinfo.format(**kwargs)
510
528
  else: text = ''
511
529
 
512
530
  return text
seolpyo_mplchart/_draw.py CHANGED
@@ -140,7 +140,9 @@ class DrawMixin(CollectionMixin):
140
140
 
141
141
  _set_key = {
142
142
  'x', 'zero', 'close_pre', 'ymax_volume',
143
- 'top_candle', 'bottom_candle', 'left_candle', 'right_candle',
143
+ 'is_up',
144
+ 'top_candle', 'bottom_candle',
145
+ 'left_candle', 'right_candle',
144
146
  'left_volume', 'right_volume',
145
147
  }
146
148
 
@@ -166,6 +168,9 @@ class DataMixin(DrawMixin):
166
168
  self.set_candlecolor = set_candlecolor
167
169
  self.set_volumecolor = set_volumecolor
168
170
 
171
+ self.chart_price_ymax = df[self.high].max() * 1.3
172
+ self.chart_volume_ymax = df[self.volume].max() * 1.3
173
+
169
174
  self._validate_column_key(df)
170
175
 
171
176
  # 오름차순 정렬
@@ -195,10 +200,9 @@ class DataMixin(DrawMixin):
195
200
  df['right_volume'] = df['x'] + self.volume_width_half
196
201
  df.loc[:, 'zero'] = 0
197
202
 
198
- df['top_candle'] = np.where(df[self.Open] <= df[self.close], df[self.close], df[self.Open])
199
- df['top_candle'] = np.where(df[self.close] < df[self.Open], df[self.Open], df[self.close])
200
- df['bottom_candle'] = np.where(df[self.Open] <= df[self.close], df[self.Open], df[self.close])
201
- df['bottom_candle'] = np.where(df[self.close] < df[self.Open], df[self.close], df[self.Open])
203
+ df['is_up'] = np.where(df[self.Open] < df[self.close], True, False)
204
+ df['top_candle'] = np.where(df['is_up'], df[self.close], df[self.Open])
205
+ df['bottom_candle'] = np.where(df['is_up'], df[self.Open], df[self.close])
202
206
 
203
207
  df['close_pre'] = df[self.close].shift(1)
204
208
  if self.volume: df['ymax_volume'] = df[self.volume] * 1.2
@@ -229,23 +233,58 @@ class CandleSegmentMixin(DataMixin):
229
233
  color_priceline = 'k'
230
234
  limit_candle = 800
231
235
 
236
+ def get_candle_segment(self, *, is_up, x, left, right, top, bottom, high, low):
237
+ """
238
+ get candle segment
239
+
240
+ Args:
241
+ is_up (bool): (True if open < close else False)
242
+ x (float): center of candle
243
+ left (float): left of candle
244
+ right (float): right of candle
245
+ top (float): top of candle(close if open < close else open)
246
+ bottom (float): bottom of candle(open if open < close else close)
247
+ high (float): top of candle wick
248
+ low (float): bottom of candle wick
249
+
250
+ Returns:
251
+ tuple[tuple[float, float]]: candle segment
252
+ """
253
+ return (
254
+ (x, high),
255
+ (x, top),
256
+ (left, top),
257
+ (left, bottom),
258
+ (x, bottom),
259
+ (x, low),
260
+ (x, bottom),
261
+ (right, bottom),
262
+ (right, top),
263
+ (x, top),
264
+ (x, high),
265
+ (x, top),
266
+ )
267
+
232
268
  def _create_candle_segments(self):
233
269
  # 캔들 세그먼트
234
- segment_candle = self.df[[
235
- 'x', self.high,
236
- 'x', 'top_candle',
237
- 'left_candle', 'top_candle',
238
- 'left_candle', 'bottom_candle',
239
- 'x', 'bottom_candle',
240
- 'x', self.low,
241
- 'x', 'bottom_candle',
242
- 'right_candle', 'bottom_candle',
243
- 'right_candle', 'top_candle',
244
- 'x', 'top_candle',
245
- 'x', self.high,
246
- 'x', 'top_candle',
247
- ]].values
248
- self.segment_candle = segment_candle.reshape(segment_candle.shape[0], 12, 2)
270
+ segment_candle = []
271
+ for x, left, right, top, bottom, is_up, high, low in zip(
272
+ self.df['x'].to_numpy().tolist(),
273
+ self.df['left_candle'].to_numpy().tolist(), self.df['right_candle'].to_numpy().tolist(),
274
+ self.df['top_candle'].to_numpy().tolist(), self.df['bottom_candle'].to_numpy().tolist(),
275
+ self.df['is_up'].to_numpy().tolist(),
276
+ self.df[self.high].to_numpy().tolist(), self.df[self.low].to_numpy().tolist(),
277
+ ):
278
+ segment_candle.append(
279
+ self.get_candle_segment(
280
+ is_up=is_up,
281
+ x=x,
282
+ left=left, right=right,
283
+ top=top, bottom=bottom,
284
+ high=high, low=low,
285
+ )
286
+ )
287
+ self.segment_candle = np.array(segment_candle)
249
288
 
250
289
  # 심지 세그먼트
251
290
  segment_wick = self.df[[
@@ -261,22 +300,26 @@ class CandleSegmentMixin(DataMixin):
261
300
  self._create_candle_color_segments()
262
301
  return
263
302
 
303
+ def add_candle_color_column(self):
304
+ columns = ['facecolor', 'edgecolor']
305
+ # 양봉
306
+ self.df.loc[:, columns] = (self.color_up, self.color_up)
307
+ if self.color_up != self.color_down:
308
+ # 음봉
309
+ self.df.loc[self.df[self.close] < self.df[self.Open], columns] = (self.color_down, self.color_down)
310
+ if self.color_up != self.color_flat and self.color_down != self.color_flat:
311
+ # 보합
312
+ self.df.loc[self.df[self.close] == self.df[self.Open], columns] = (self.color_flat, self.color_flat)
313
+ if self.color_up != self.color_up_down:
314
+ # 양봉(비우기)
315
+ self.df.loc[(self.df['facecolor'] == self.color_up) & (self.df[self.close] <= self.df['close_pre']), 'facecolor'] = self.color_up_down
316
+ if self.color_down != self.color_down_up:
317
+ # 음봉(비우기)
318
+ self.df.loc[(self.df['facecolor'] == self.color_down) & (self.df['close_pre'] <= self.df[self.close]), ['facecolor']] = self.color_down_up
319
+ return
320
+
264
321
  def _create_candle_color_segments(self):
265
- if self.set_candlecolor:
266
- # 양봉
267
- self.df.loc[:, ['facecolor', 'edgecolor']] = (self.color_up, self.color_up)
268
- if self.color_up != self.color_down:
269
- # 음봉
270
- self.df.loc[self.df[self.close] < self.df[self.Open], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
271
- if self.color_up != self.color_flat and self.color_down != self.color_flat:
272
- # 보합
273
- self.df.loc[self.df[self.close] == self.df[self.Open], ['facecolor', 'edgecolor']] = (self.color_flat, self.color_flat)
274
- if self.color_up != self.color_up_down:
275
- # 양봉(비우기)
276
- self.df.loc[(self.df['facecolor'] == self.color_up) & (self.df[self.close] <= self.df['close_pre']), 'facecolor'] = self.color_up_down
277
- if self.color_down != self.color_down_up:
278
- # 음봉(비우기)
279
- self.df.loc[(self.df['facecolor'] == self.color_down) & (self.df['close_pre'] <= self.df[self.close]), ['facecolor']] = self.color_down_up
322
+ if self.set_candlecolor: self.add_candle_color_column()
280
323
 
281
324
  self.facecolor_candle = self.df['facecolor'].values
282
325
  self.edgecolor_candle = self.df['edgecolor'].values
@@ -362,15 +405,39 @@ class MaSegmentMixin(CandleSegmentMixin):
362
405
  class VolumeSegmentMixin(MaSegmentMixin):
363
406
  limit_volume = 200
364
407
 
408
+ def get_volume_segment(self, *, x, left, right, top):
409
+ """
410
+ get volume bar segment
411
+
412
+ Args:
413
+ x (float): center of volume bar
414
+ left (float): left of volume bar
415
+ right (float): right of volume bar
416
+ top (float): top of volume bar
417
+
418
+ Returns:
419
+ tuple[tuple[float, float]]: volume bar segment
420
+ """
421
+ return (
422
+ (left, top),
423
+ (left, 0),
424
+ (right, 0),
425
+ (right, top),
426
+ (left, top),
427
+ )
428
+
365
429
  def _create_volume_segments(self):
366
430
  # 거래량 바 세그먼트
367
- segment_volume = self.df[[
368
- 'left_volume', 'zero',
369
- 'left_volume', self.volume,
370
- 'right_volume', self.volume,
371
- 'right_volume', 'zero',
372
- ]].values
373
- self.segment_volume = segment_volume.reshape(segment_volume.shape[0], 4, 2)
431
+ segment_volume = []
432
+ for x, left, right, top in zip(
433
+ self.df['x'].to_numpy().tolist(),
434
+ self.df['left_volume'].to_numpy().tolist(), self.df['right_volume'].to_numpy().tolist(),
435
+ self.df[self.volume].to_numpy().tolist(),
436
+ ):
437
+ segment_volume.append(
438
+ self.get_volume_segment(x=x, left=left, right=right, top=top)
439
+ )
440
+ self.segment_volume = np.array(segment_volume)
374
441
 
375
442
  # 거래량 심지 세그먼트
376
443
  segment_volume_wick = self.df[[
@@ -382,19 +449,22 @@ class VolumeSegmentMixin(MaSegmentMixin):
382
449
  self._create_volume_color_segments()
383
450
  return
384
451
 
452
+ def add_volume_color_column(self):
453
+ columns = ['facecolor_volume', 'edgecolor_volume']
454
+ # 거래량
455
+ self.df.loc[:, columns] = (self.color_volume_up, self.color_volume_up)
456
+ if self.color_up != self.color_down:
457
+ # 전일대비 하락
458
+ condition = self.df[self.close] < self.df['close_pre']
459
+ self.df.loc[condition, columns] = (self.color_volume_down, self.color_volume_down)
460
+ if self.color_up != self.color_flat:
461
+ # 전일과 동일
462
+ condition = self.df[self.close] == self.df['close_pre']
463
+ self.df.loc[condition, columns] = (self.color_volume_flat, self.color_volume_flat)
464
+ return
465
+
385
466
  def _create_volume_color_segments(self):
386
- if self.set_volumecolor:
387
- columns = ['facecolor_volume', 'edgecolor_volume']
388
- # 거래량
389
- self.df.loc[:, columns] = (self.color_volume_up, self.color_volume_up)
390
- if self.color_up != self.color_down:
391
- # 전일대비 하락
392
- condition = self.df[self.close] < self.df['close_pre']
393
- self.df.loc[condition, columns] = (self.color_volume_down, self.color_volume_down)
394
- if self.color_up != self.color_flat:
395
- # 전일과 동일
396
- condition = self.df[self.close] == self.df['close_pre']
397
- self.df.loc[condition, columns] = (self.color_volume_flat, self.color_volume_flat)
467
+ if self.set_volumecolor: self.add_volume_color_column()
398
468
 
399
469
  self.facecolor_volume = self.df['facecolor_volume'].values
400
470
  self.edgecolor_volume = self.df['edgecolor_volume'].values
seolpyo_mplchart/test.py CHANGED
@@ -1,11 +1,71 @@
1
+ import json
1
2
  import sys
2
3
  from pathlib import Path
3
4
 
5
+ import pandas as pd
6
+
4
7
  sys.path.insert(0, Path(__file__).parent.parent.__str__())
5
8
  # print(f'{sys.path=}')
6
9
 
7
- from seolpyo_mplchart import sample
10
+ import seolpyo_mplchart as mc
11
+
12
+
13
+ class Chart(mc.SliderChart):
14
+ format_candleinfo = mc.format_candleinfo_ko + '\nCustom info: {ci}'
15
+ format_volumeinfo = mc.format_volumeinfo_ko
16
+ min_distance = 5
17
+
18
+ def __init__(self, *args, **kwargs):
19
+ super().__init__(*args, **kwargs)
20
+ self.collection_candle.set_linewidth(1.5)
21
+ return
22
+
23
+ def get_info_kwargs(self, is_price, **kwargs):
24
+ if is_price:
25
+ kwargs['ci'] = 'You can add Custom text Info or Change text info.'
26
+ kwargs['close'] = 'You can Change close price info.'
27
+ return kwargs
28
+
29
+ def get_candle_segment(self, *, x, left, right, top, bottom, is_up, high, low):
30
+ if is_up:
31
+ return (
32
+ (x, high),
33
+ (x, top),
34
+ (right, top),
35
+ (x, top),
36
+ (x, bottom),
37
+ (left, bottom),
38
+ (x, bottom),
39
+ (x, low),
40
+ (x, high)
41
+ )
42
+ else:
43
+ return (
44
+ (x, high),
45
+ (x, bottom),
46
+ (right, bottom),
47
+ (x, bottom),
48
+ (x, top),
49
+ (left, top),
50
+ (x, top),
51
+ (x, low),
52
+ (x, high)
53
+ )
54
+
55
+
56
+
57
+ C = Chart()
58
+ path_file = Path(__file__).parent / 'sample/samsung.txt'
59
+ # C.format_candleinfo = mc.format_candleinfo_ko
60
+ # C.format_volumeinfo = mc.format_volumeinfo_ko
61
+ # C.volume = None
62
+
8
63
 
64
+ with open(path_file, 'r', encoding='utf-8') as txt:
65
+ data = json.load(txt)
66
+ df = pd.DataFrame(data)
9
67
 
10
- sample()
68
+ C.set_data(df)
11
69
 
70
+ mc.show()
71
+ mc.close()
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: seolpyo-mplchart
3
- Version: 1.4.0.3
3
+ Version: 1.4.0.4
4
4
  Summary: Fast candlestick chart using Python. Includes navigator, slider, navigation, and text information display functions
5
5
  Author-email: white-seolpyo <white-seolpyo@naver.com>
6
6
  License: MIT License
@@ -32,9 +32,9 @@ Ethereum: 0x1c5fb8a5e0b1153cd4116c91736bd16fabf83520
32
32
 
33
33
 
34
34
  # Document
35
- [English Document](https://white.seolpyo.com/entry/148/)
35
+ [English Document](https://white.seolpyo.com/entry/148/?from=pypi)
36
36
 
37
- [한글 설명서](https://white.seolpyo.com/entry/147/)
37
+ [한글 설명서](https://white.seolpyo.com/entry/147/?from=pypi)
38
38
 
39
39
 
40
40
  # Sample Image
@@ -50,6 +50,8 @@ Ethereum: 0x1c5fb8a5e0b1153cd4116c91736bd16fabf83520
50
50
  ## Korean format sample
51
51
  ![korean sample](https://raw.githubusercontent.com/white-seolpyo/seolpyo-mplchart/refs/heads/main/images/sample%20kor.png)
52
52
 
53
+ ## change Candle shape sample
54
+ ![Candle shape sample](https://github.com/white-seolpyo/seolpyo-mplchart/blob/main/images/change%20candle%20segment.png)
53
55
 
54
56
  # 40,000 data sample
55
57
  ![40,000 sample](https://raw.githubusercontent.com/white-seolpyo/seolpyo-mplchart/refs/heads/main/images/40000.gif)
@@ -1,17 +1,17 @@
1
1
  seolpyo_mplchart/__init__.py,sha256=1rY6PP1OMMr1d_5x7dlAD2ImQAXChT5spvB45j6yDp8,18255
2
2
  seolpyo_mplchart/_base.py,sha256=3iG4AVwHR_h3rh6c1oJahWt7NvBpBFrS0bUZd4PaHfY,3921
3
- seolpyo_mplchart/_cursor.py,sha256=051qy_fn0zypA5ceYKzSF8XMypdjqV3GE35kmtKeCU4,22845
4
- seolpyo_mplchart/_draw.py,sha256=TGPo80yJocmXbWoCfzYKlWK6ntBElvb9IR8O54A2X5w,21174
3
+ seolpyo_mplchart/_cursor.py,sha256=w3ndsyfFMRp8nnkAEVXtJH8-YLm1r-mA8bCdS82iSWE,23442
4
+ seolpyo_mplchart/_draw.py,sha256=iBaFkllpaXewjECU82gZ0RQm8qTG0c1kP0PZdq8PtFo,23242
5
5
  seolpyo_mplchart/_slider.py,sha256=4zcanX9KSdtjn12IE3zKERaqTh07x42LKfFD2KEMcB8,23309
6
6
  seolpyo_mplchart/base.py,sha256=0qdImsIMPzTTkkHzPv479BVe_ojrn45FidGE46eT5x4,3797
7
7
  seolpyo_mplchart/cursor.py,sha256=qq1WJJa7vCE5C2XeGBECt2XqxR_WxfybZZ5u6zVx5Ps,20415
8
8
  seolpyo_mplchart/draw.py,sha256=MiqDhbMJOSlWD6qdAghsyrZwCixcwm4nfmiinvwtt-o,21520
9
9
  seolpyo_mplchart/slider.py,sha256=-jZ6D23orDj3NmtnOviO1NRx4BrRxWvwTV5pzGQuWNI,22188
10
- seolpyo_mplchart/test.py,sha256=TFnIXphJsl-B7iIhBh7-PZKUz2Gjh7mwNwrk8aUS4SA,180
10
+ seolpyo_mplchart/test.py,sha256=VWpAbgALqeW04LKvvf8-lLK6OwwJWuMlqfTB5ylTpnY,1830
11
11
  seolpyo_mplchart/utils.py,sha256=a3XycRBTndrsjBw_1VKTxbSvOGpVYXHRK87v7azgRe8,1433
12
12
  seolpyo_mplchart/data/apple.txt,sha256=0izAfweu1lLsC0IwVthdVlo9reG8KGbKGTSX5knI5Zc,1380864
13
13
  seolpyo_mplchart/data/samsung.txt,sha256=UejaSkbzr4E5K3lkelCT0yJiWUPfmViBEaTyoXyphIs,2476424
14
- seolpyo_mplchart-1.4.0.3.dist-info/METADATA,sha256=ptAsq-0kfzKL5Gyg3pjbSkuPC_GKBe2LJvO8eT0wO18,2484
15
- seolpyo_mplchart-1.4.0.3.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
16
- seolpyo_mplchart-1.4.0.3.dist-info/top_level.txt,sha256=KgqFn7rKWize7OjMaTCHxKm9ie6vqnyb5c8fN7y_tSo,17
17
- seolpyo_mplchart-1.4.0.3.dist-info/RECORD,,
14
+ seolpyo_mplchart-1.4.0.4.dist-info/METADATA,sha256=_bkVwBE4cCryceC2bHQPZSgGG13xFigx80EzOfogrYQ,2657
15
+ seolpyo_mplchart-1.4.0.4.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
16
+ seolpyo_mplchart-1.4.0.4.dist-info/top_level.txt,sha256=KgqFn7rKWize7OjMaTCHxKm9ie6vqnyb5c8fN7y_tSo,17
17
+ seolpyo_mplchart-1.4.0.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.1.0)
2
+ Generator: setuptools (79.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5