siat 3.11.4__py3-none-any.whl → 3.11.6__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.
- siat/common.py +83 -2
- siat/save2docx.py +62 -5
- siat/save2pdf-20250716.py +173 -0
- siat/save2pdf-playwright-20250714.py +202 -0
- siat/save2pdf.py +87 -86
- {siat-3.11.4.dist-info → siat-3.11.6.dist-info}/METADATA +3 -3
- {siat-3.11.4.dist-info → siat-3.11.6.dist-info}/RECORD +10 -8
- {siat-3.11.4.dist-info → siat-3.11.6.dist-info}/WHEEL +1 -1
- {siat-3.11.4.dist-info → siat-3.11.6.dist-info}/LICENSE +0 -0
- {siat-3.11.4.dist-info → siat-3.11.6.dist-info}/top_level.txt +0 -0
siat/common.py
CHANGED
@@ -3013,11 +3013,12 @@ def fix_package(file='stooq.py', package='pandas_datareader'):
|
|
3013
3013
|
if __name__=='__main__':
|
3014
3014
|
file='stock_info.pickle'
|
3015
3015
|
package='siat'
|
3016
|
+
mode='read'
|
3016
3017
|
developer=False
|
3017
3018
|
|
3018
3019
|
file_position()
|
3019
3020
|
|
3020
|
-
def
|
3021
|
+
def file_position_x(file='stock_info.pickle',package='siat',mode='read'):
|
3021
3022
|
"""
|
3022
3023
|
功能:给定文件名file,返回其路径
|
3023
3024
|
注意:执行本程序可能需要系统管理员权限,可以系统管理员权限启动Jupyter或Spyder
|
@@ -3027,6 +3028,7 @@ def file_position(file='stock_info.pickle',package='siat',mode='read'):
|
|
3027
3028
|
stooq.py pandas_datareader
|
3028
3029
|
bond_zh_sina.py akshare
|
3029
3030
|
|
3031
|
+
问题:在Python 3.13上后台运行出错,暂时废弃!
|
3030
3032
|
"""
|
3031
3033
|
#判断操作系统
|
3032
3034
|
import sys; czxt=sys.platform
|
@@ -3061,6 +3063,57 @@ def file_position(file='stock_info.pickle',package='siat',mode='read'):
|
|
3061
3063
|
else:
|
3062
3064
|
return objfile
|
3063
3065
|
#==============================================================================
|
3066
|
+
import sys
|
3067
|
+
import pickle
|
3068
|
+
import importlib
|
3069
|
+
|
3070
|
+
def file_position(file='stock_info.pickle', package='siat', mode='read'):
|
3071
|
+
"""
|
3072
|
+
功能:给定文件名file,返回其路径或读取其内容
|
3073
|
+
参数:
|
3074
|
+
file - 目标文件名
|
3075
|
+
package - 存放该文件的 Python 包名
|
3076
|
+
mode - 'read' 则加载并返回 pickle 中的对象,否则返回文件路径
|
3077
|
+
注意:执行本程序可能需要系统管理员权限
|
3078
|
+
"""
|
3079
|
+
# 1. 判断操作系统
|
3080
|
+
czxt = sys.platform
|
3081
|
+
if czxt in ('win32', 'win64'):
|
3082
|
+
os_type = 'windows'
|
3083
|
+
elif czxt == 'darwin':
|
3084
|
+
os_type = 'mac'
|
3085
|
+
elif czxt.startswith('linux'):
|
3086
|
+
os_type = 'linux'
|
3087
|
+
else:
|
3088
|
+
os_type = 'windows'
|
3089
|
+
|
3090
|
+
# 2. 动态导入 package
|
3091
|
+
try:
|
3092
|
+
pkg = importlib.import_module(package)
|
3093
|
+
except ImportError as e:
|
3094
|
+
raise ImportError(f"无法导入包 '{package}': {e}")
|
3095
|
+
|
3096
|
+
# 3. 获取 package 的安装路径
|
3097
|
+
try:
|
3098
|
+
objpath = pkg.__path__[0]
|
3099
|
+
except (AttributeError, IndexError):
|
3100
|
+
# 如果是单文件模块,退而求其次取 __file__ 的目录
|
3101
|
+
objpath = importlib.util.find_spec(package).origin
|
3102
|
+
objpath = objpath.rsplit('/', 1)[0]
|
3103
|
+
|
3104
|
+
# 4. 拼接目标文件路径
|
3105
|
+
if os_type == 'windows':
|
3106
|
+
objpath_norm = objpath.replace('\\', '/')
|
3107
|
+
objfile = objpath_norm + '/' + file
|
3108
|
+
else:
|
3109
|
+
objfile = objpath + '/' + file
|
3110
|
+
|
3111
|
+
# 5. 根据 mode 读取或返回路径
|
3112
|
+
if mode == 'read':
|
3113
|
+
with open(objfile, 'rb') as f:
|
3114
|
+
return pickle.load(f)
|
3115
|
+
else:
|
3116
|
+
return objfile
|
3064
3117
|
|
3065
3118
|
|
3066
3119
|
#==============================================================================
|
@@ -4897,8 +4950,28 @@ def ttest(sample1,sample2):
|
|
4897
4950
|
return round(p_value,4)
|
4898
4951
|
|
4899
4952
|
#==============================================================================
|
4953
|
+
import sys,os
|
4954
|
+
|
4955
|
+
# —— 在程序最开始处,打印并校验 Python 版本 ——
|
4956
|
+
def _check_python_version_jupyter2pdf():
|
4957
|
+
ver = sys.version_info
|
4958
|
+
version_str = f"{ver.major}.{ver.minor}.{ver.micro}"
|
4959
|
+
#print(f"使用的 Python 版本:{version_str}")
|
4960
|
+
|
4961
|
+
# 只允许 3.7 <= 版本 < 3.13
|
4962
|
+
usable=True
|
4963
|
+
if not (ver.major == 3 and 7 <= ver.minor <= 12):
|
4964
|
+
usable = False
|
4965
|
+
print(f"不支持的 Python 版本:{version_str},仅支持 Python 3.7–3.12")
|
4966
|
+
print(f"解决方案:改用ipynb2pdf或ipynb2docx")
|
4967
|
+
"""
|
4968
|
+
raise RuntimeError(
|
4969
|
+
f"不支持的 Python 版本:{version_str}。"
|
4970
|
+
" 请使用 Python 3.7–3.12。"
|
4971
|
+
)
|
4972
|
+
"""
|
4900
4973
|
|
4901
|
-
|
4974
|
+
return usable
|
4902
4975
|
|
4903
4976
|
async def jupyter2pdf(notebook_path, output_pdf_path, size="A3"):
|
4904
4977
|
"""
|
@@ -4915,6 +4988,9 @@ async def jupyter2pdf(notebook_path, output_pdf_path, size="A3"):
|
|
4915
4988
|
await convert_notebook_to_pdf(notebook_path, output_pdf_path)
|
4916
4989
|
注意3:notebook_path和output_pdf_path中可以带路径
|
4917
4990
|
"""
|
4991
|
+
if not _check_python_version_jupyter2pdf():
|
4992
|
+
return
|
4993
|
+
|
4918
4994
|
size=size.upper()
|
4919
4995
|
if not size in ['A4','A3','letter']:
|
4920
4996
|
size='A3'
|
@@ -4994,6 +5070,8 @@ async def jupyter2pdf2(notebook_dir, notebook_file):
|
|
4994
5070
|
注意2:调用本函数的格式是异步await开头,例如:
|
4995
5071
|
await jupyter2pdf2(notebook_dir, notebook_file)
|
4996
5072
|
"""
|
5073
|
+
if not _check_python_version_jupyter2pdf():
|
5074
|
+
return
|
4997
5075
|
|
4998
5076
|
# 路径分割符号
|
4999
5077
|
if ('/' in notebook_dir) and not ('\\' in notebook_dir):
|
@@ -5123,6 +5201,9 @@ async def jupyter2pdf3(notebook_path):
|
|
5123
5201
|
await jupyter2pdf3(notebook_path)
|
5124
5202
|
"""
|
5125
5203
|
DEBUG=False
|
5204
|
+
|
5205
|
+
if not _check_python_version_jupyter2pdf():
|
5206
|
+
return
|
5126
5207
|
|
5127
5208
|
import os,sys
|
5128
5209
|
# 分离目录和文件名
|
siat/save2docx.py
CHANGED
@@ -21,8 +21,10 @@ SIAT:Security Investment Analysis Tool
|
|
21
21
|
"""
|
22
22
|
|
23
23
|
#==============================================================================
|
24
|
+
#关闭所有警告
|
25
|
+
import warnings; warnings.filterwarnings('ignore')
|
24
26
|
|
25
|
-
import os
|
27
|
+
import os,sys
|
26
28
|
import errno
|
27
29
|
import tempfile
|
28
30
|
import subprocess
|
@@ -39,6 +41,44 @@ from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
39
41
|
from docx.enum.table import WD_TABLE_ALIGNMENT
|
40
42
|
from docx.text.paragraph import Paragraph
|
41
43
|
|
44
|
+
import logging
|
45
|
+
|
46
|
+
import contextlib
|
47
|
+
import io
|
48
|
+
|
49
|
+
import time
|
50
|
+
from IPython.display import display, Javascript
|
51
|
+
|
52
|
+
# —— 新增:Notebook 强制保存:貌似不管用 ——
|
53
|
+
def _save_current_notebook():
|
54
|
+
"""
|
55
|
+
在浏览器端触发一次保存:兼容 Classic Notebook、Lab 3.x/4.x。
|
56
|
+
"""
|
57
|
+
js = """
|
58
|
+
(function() {
|
59
|
+
// Classic Notebook
|
60
|
+
if (window.Jupyter && Jupyter.notebook) {
|
61
|
+
Jupyter.notebook.save_checkpoint();
|
62
|
+
}
|
63
|
+
// JupyterLab >=3: 用 app.commands
|
64
|
+
else if (window.jupyterapp && jupyterapp.commands) {
|
65
|
+
jupyterapp.commands.execute('docmanager:save');
|
66
|
+
}
|
67
|
+
// JupyterLab <=2 或其他
|
68
|
+
else if (window.require) {
|
69
|
+
require(['@jupyterlab/docmanager'], function(docManager) {
|
70
|
+
docManager.save();
|
71
|
+
});
|
72
|
+
}
|
73
|
+
})();
|
74
|
+
"""
|
75
|
+
try:
|
76
|
+
display(Javascript(js))
|
77
|
+
time.sleep(0.5) # 给浏览器一点时间写盘
|
78
|
+
except Exception:
|
79
|
+
pass
|
80
|
+
|
81
|
+
|
42
82
|
# 预设纸张尺寸(单位:毫米)
|
43
83
|
PAGE_SIZES = {"A4": (210, 297), "A3": (297, 420)}
|
44
84
|
|
@@ -116,6 +156,10 @@ def convert_ipynb_to_docx(ipynb_path, docx_path=None, page_size="A3"):
|
|
116
156
|
5. 表格等分列宽居中;图像放大至页宽并居中
|
117
157
|
6. 若目标 docx 正被打开,抛出提示“请先关闭文件”
|
118
158
|
"""
|
159
|
+
# 0. 强制保存当前 Notebook,貌似不管用,需要手动保存当前的Notebook
|
160
|
+
#print("Saving current ipynb ...")
|
161
|
+
#_save_current_notebook()
|
162
|
+
|
119
163
|
# ---- 1. 检查输入 & 输出路径 ----
|
120
164
|
if not os.path.isfile(ipynb_path):
|
121
165
|
raise FileNotFoundError(f"找不到输入文件:{ipynb_path}")
|
@@ -148,9 +192,20 @@ def convert_ipynb_to_docx(ipynb_path, docx_path=None, page_size="A3"):
|
|
148
192
|
# ---- 4. Notebook → HTML(嵌入图像) ----
|
149
193
|
exporter = HTMLExporter()
|
150
194
|
exporter.embed_images = True
|
151
|
-
|
195
|
+
|
196
|
+
# 关闭所有小于 CRITICAL 的日志
|
197
|
+
logging.disable(logging.CRITICAL)
|
198
|
+
|
199
|
+
buf = io.StringIO()
|
200
|
+
# 屏蔽 stderr和stdout
|
201
|
+
with contextlib.redirect_stderr(buf):
|
202
|
+
with contextlib.redirect_stdout(buf):
|
203
|
+
html_body, _ = exporter.from_notebook_node(nb)
|
152
204
|
html = f"<h1>{title}</h1>\n" + html_body
|
153
205
|
|
206
|
+
# 恢复日志
|
207
|
+
#logging.disable(logging.NOTSET)
|
208
|
+
|
154
209
|
# ---- 5. HTML → DOCX via Pandoc(或 subprocess fallback) ----
|
155
210
|
try:
|
156
211
|
pypandoc.convert_text(
|
@@ -338,9 +393,11 @@ def ipynb2docx(ipynb_path, page_size="A3"):
|
|
338
393
|
print(f"Converting to docx ...")
|
339
394
|
|
340
395
|
result = convert_ipynb_to_docx(ipynb_path, docx_path=None, page_size=page_size)
|
341
|
-
|
342
|
-
|
343
|
-
print(f"
|
396
|
+
dirpath, filename = os.path.split(result)
|
397
|
+
|
398
|
+
print(f"{filename} is created with TOC in {page_size} size")
|
399
|
+
print(f"It is in {dirpath}")
|
400
|
+
print(f"Note: may need further adjustments in formatting")
|
344
401
|
|
345
402
|
return
|
346
403
|
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
本模块功能:转换ipynb文件为pdf,带有可跳转的目录(目前一级标题定位还不准确,二级以下目录定位较准确,但已可用)
|
4
|
+
所属工具包:证券投资分析工具SIAT
|
5
|
+
SIAT:Security Investment Analysis Tool
|
6
|
+
创建日期:2025年7月8日
|
7
|
+
最新修订日期:2025年7月8日
|
8
|
+
作者:王德宏 (WANG Dehong, Peter)
|
9
|
+
作者单位:北京外国语大学国际商学院
|
10
|
+
作者邮件:wdehong2000@163.com
|
11
|
+
版权所有:王德宏
|
12
|
+
用途限制:仅限研究与教学使用。
|
13
|
+
特别声明:作者不对使用本工具进行证券投资导致的任何损益负责!
|
14
|
+
"""
|
15
|
+
|
16
|
+
#==============================================================================
|
17
|
+
|
18
|
+
# 首次运行前,请安装依赖:
|
19
|
+
# !pip install nbformat nbconvert playwright pymupdf nest_asyncio
|
20
|
+
# !playwright install
|
21
|
+
|
22
|
+
#关闭所有警告
|
23
|
+
import warnings; warnings.filterwarnings('ignore')
|
24
|
+
|
25
|
+
# 能够在Python 3.13下运行了!
|
26
|
+
import os
|
27
|
+
import re
|
28
|
+
import sys
|
29
|
+
import time
|
30
|
+
import tempfile
|
31
|
+
import subprocess
|
32
|
+
import nbformat
|
33
|
+
from nbconvert import HTMLExporter
|
34
|
+
import fitz # PyMuPDF
|
35
|
+
from pathlib import Path
|
36
|
+
import contextlib
|
37
|
+
import io
|
38
|
+
from IPython import get_ipython
|
39
|
+
from ipykernel.zmqshell import ZMQInteractiveShell
|
40
|
+
from nbformat.notebooknode import NotebookNode
|
41
|
+
from playwright.sync_api import sync_playwright
|
42
|
+
|
43
|
+
# ----------------------------------------------------------------
|
44
|
+
# 1) 新增:把内存中的 NotebookNode 写到临时 .ipynb
|
45
|
+
# ----------------------------------------------------------------
|
46
|
+
def _dump_notebook_to_file(nb_node: NotebookNode) -> str:
|
47
|
+
"""
|
48
|
+
把传进来的 NotebookNode(如 globals().get("__session__"))写到一个临时 .ipynb 文件,
|
49
|
+
返回该临时文件的路径。
|
50
|
+
"""
|
51
|
+
tf = tempfile.NamedTemporaryFile(
|
52
|
+
suffix=".ipynb", delete=False, mode="w", encoding="utf-8"
|
53
|
+
)
|
54
|
+
nbformat.write(nb_node, tf)
|
55
|
+
tf.flush()
|
56
|
+
tf.close()
|
57
|
+
return tf.name
|
58
|
+
|
59
|
+
# ----------------------------------------------------------------
|
60
|
+
# 2) 主函数:接受文件路径或 NotebookNode
|
61
|
+
# ----------------------------------------------------------------
|
62
|
+
def ipynb2pdf(source, *, temp_cleanup: bool = True) -> str:
|
63
|
+
"""
|
64
|
+
将 .ipynb 转为带书签的 PDF,返回最终 PDF 路径。
|
65
|
+
|
66
|
+
参数 source:
|
67
|
+
- 如果是字符串,视为已有 .ipynb 文件的路径;
|
68
|
+
- 如果是 NotebookNode,就把它 dump 到临时 .ipynb,再做后续处理。
|
69
|
+
|
70
|
+
temp_cleanup:是否清理临时 .ipynb 文件,默认为 True。
|
71
|
+
"""
|
72
|
+
# —— 如果传进来的是 NotebookNode,先 dump 到临时文件 ——
|
73
|
+
if isinstance(source, NotebookNode):
|
74
|
+
ipynb_path = _dump_notebook_to_file(source)
|
75
|
+
# 后面用完,最后再删
|
76
|
+
cleanup_ipynb = temp_cleanup
|
77
|
+
else:
|
78
|
+
ipynb_path = str(source)
|
79
|
+
cleanup_ipynb = False
|
80
|
+
|
81
|
+
if not os.path.isfile(ipynb_path):
|
82
|
+
raise FileNotFoundError(f"找不到文件:{ipynb_path}")
|
83
|
+
output_pdf = ipynb_path[:-6] + ".pdf"
|
84
|
+
|
85
|
+
print("Converting to PDF ...")
|
86
|
+
|
87
|
+
# 3. 读内存/磁盘中的 notebook,提取 TOC
|
88
|
+
nb = nbformat.read(ipynb_path, as_version=4)
|
89
|
+
toc = _extract_toc(nb)
|
90
|
+
|
91
|
+
# 4. 转 HTML,吞掉 alt‐text 警告
|
92
|
+
exporter = HTMLExporter()
|
93
|
+
buf = io.StringIO()
|
94
|
+
with contextlib.redirect_stdout(buf):
|
95
|
+
html_body, _ = exporter.from_notebook_node(nb)
|
96
|
+
|
97
|
+
# 5. 写临时 HTML / PDF
|
98
|
+
with tempfile.NamedTemporaryFile("w", suffix=".html", encoding="utf-8", delete=False) as th:
|
99
|
+
th.write(html_body)
|
100
|
+
html_path = th.name
|
101
|
+
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tp:
|
102
|
+
tmp_pdf = tp.name
|
103
|
+
|
104
|
+
# 6. 子进程里用 Playwright 同步渲染 PDF
|
105
|
+
script = f"""
|
106
|
+
import sys
|
107
|
+
from playwright.sync_api import sync_playwright
|
108
|
+
p = sync_playwright().start()
|
109
|
+
b = p.chromium.launch()
|
110
|
+
pg = b.new_page()
|
111
|
+
pg.goto(r"file://{html_path}")
|
112
|
+
pg.pdf(path=r"{tmp_pdf}", format="A3", print_background=True,
|
113
|
+
margin={{"top":"20mm","bottom":"20mm","left":"20mm","right":"20mm"}})
|
114
|
+
b.close(); p.stop()
|
115
|
+
"""
|
116
|
+
subprocess.run([sys.executable, "-c", script], check=True)
|
117
|
+
|
118
|
+
# 7. 用 PyMuPDF 加书签
|
119
|
+
_add_bookmarks(tmp_pdf, output_pdf, toc)
|
120
|
+
|
121
|
+
# 8. 清理临时文件
|
122
|
+
os.unlink(html_path)
|
123
|
+
os.unlink(tmp_pdf)
|
124
|
+
if cleanup_ipynb:
|
125
|
+
os.unlink(ipynb_path)
|
126
|
+
|
127
|
+
print(f"✅ {Path(output_pdf).name} created with TOC")
|
128
|
+
print(f"✅ in {Path(output_pdf).parent}")
|
129
|
+
return output_pdf
|
130
|
+
|
131
|
+
# ----------------------------------------------------------------
|
132
|
+
# 3) 工具函数:提取目录、写书签
|
133
|
+
# ----------------------------------------------------------------
|
134
|
+
def _extract_toc(nb_node) -> list[tuple[int, str]]:
|
135
|
+
toc = []
|
136
|
+
for cell in nb_node.cells:
|
137
|
+
if cell.cell_type != "markdown":
|
138
|
+
continue
|
139
|
+
first = cell.source.strip().splitlines()[0]
|
140
|
+
m = re.match(r"^(#{1,6})\s+(.*)", first)
|
141
|
+
if m:
|
142
|
+
toc.append((len(m.group(1)), m.group(2).strip()))
|
143
|
+
return toc
|
144
|
+
|
145
|
+
def _add_bookmarks(input_pdf: str, output_pdf: str, toc: list[tuple[int, str]]):
|
146
|
+
doc = fitz.open(input_pdf)
|
147
|
+
outline = []
|
148
|
+
for level, title in toc:
|
149
|
+
pg = next(
|
150
|
+
(i+1 for i in range(doc.page_count)
|
151
|
+
if title in doc.load_page(i).get_text()),
|
152
|
+
1
|
153
|
+
)
|
154
|
+
outline.append([level, title, pg])
|
155
|
+
doc.set_toc(outline)
|
156
|
+
doc.save(output_pdf)
|
157
|
+
|
158
|
+
|
159
|
+
# 使用示例(另起一个 cell 运行):
|
160
|
+
# ipynb = globals().get("__session__")
|
161
|
+
# ipynb2pdf(ipynb)
|
162
|
+
|
163
|
+
|
164
|
+
#==============================================================================
|
165
|
+
|
166
|
+
#==============================================================================
|
167
|
+
#==============================================================================
|
168
|
+
#==============================================================================
|
169
|
+
#==============================================================================
|
170
|
+
#==============================================================================
|
171
|
+
#==============================================================================
|
172
|
+
#==============================================================================
|
173
|
+
#==============================================================================
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
本模块功能:转换ipynb文件为pdf,带有可跳转的目录(目前一级标题定位还不准确,二级以下目录定位较准确,但已可用)
|
4
|
+
所属工具包:证券投资分析工具SIAT
|
5
|
+
SIAT:Security Investment Analysis Tool
|
6
|
+
创建日期:2025年7月8日
|
7
|
+
最新修订日期:2025年7月8日
|
8
|
+
作者:王德宏 (WANG Dehong, Peter)
|
9
|
+
作者单位:北京外国语大学国际商学院
|
10
|
+
作者邮件:wdehong2000@163.com
|
11
|
+
版权所有:王德宏
|
12
|
+
用途限制:仅限研究与教学使用。
|
13
|
+
特别声明:作者不对使用本工具进行证券投资导致的任何损益负责!
|
14
|
+
"""
|
15
|
+
|
16
|
+
#==============================================================================
|
17
|
+
|
18
|
+
# 首次运行前,请安装依赖:
|
19
|
+
# !pip install nbformat nbconvert playwright pymupdf nest_asyncio
|
20
|
+
# !playwright install
|
21
|
+
|
22
|
+
# 针对Python 3.13在Windows下的修复
|
23
|
+
# 在 Notebook 首格运行:
|
24
|
+
import sys, asyncio
|
25
|
+
|
26
|
+
if sys.platform.startswith("win"):
|
27
|
+
# SelectorEventLoop 无法启动 subprocess,改用 ProactorEventLoop
|
28
|
+
asyncio.set_event_loop_policy(
|
29
|
+
asyncio.WindowsProactorEventLoopPolicy()
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
# 下面在Python < 3.13可正常运行
|
34
|
+
import os
|
35
|
+
import re
|
36
|
+
import tempfile
|
37
|
+
import asyncio
|
38
|
+
|
39
|
+
import nest_asyncio
|
40
|
+
import nbformat
|
41
|
+
from nbconvert import HTMLExporter
|
42
|
+
from playwright.async_api import async_playwright
|
43
|
+
import fitz # PyMuPDF
|
44
|
+
|
45
|
+
nest_asyncio.apply() # 使 asyncio.run 在 Notebook 中可用
|
46
|
+
|
47
|
+
def ipynb2pdf(ipynb_path: str) -> str:
|
48
|
+
"""
|
49
|
+
将 .ipynb 转为带可跳转目录书签的 PDF。
|
50
|
+
返回生成的 PDF 文件路径。
|
51
|
+
"""
|
52
|
+
if not os.path.isfile(ipynb_path):
|
53
|
+
raise FileNotFoundError(f"找不到文件:{ipynb_path}")
|
54
|
+
output_pdf = ipynb_path[:-6] + ".pdf"
|
55
|
+
|
56
|
+
print(f"Converting to PDF ...")
|
57
|
+
|
58
|
+
# 1. 读 notebook → 提取目录结构
|
59
|
+
nb = nbformat.read(ipynb_path, as_version=4)
|
60
|
+
toc = _extract_toc(nb)
|
61
|
+
|
62
|
+
# 2. nb → HTML
|
63
|
+
exporter = HTMLExporter()
|
64
|
+
html_body, _ = exporter.from_notebook_node(nb)
|
65
|
+
|
66
|
+
# 3. 临时写 HTML / PDF
|
67
|
+
with tempfile.NamedTemporaryFile("w", suffix=".html", encoding="utf-8", delete=False) as th:
|
68
|
+
th.write(html_body)
|
69
|
+
html_path = th.name
|
70
|
+
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tp:
|
71
|
+
tmp_pdf = tp.name
|
72
|
+
|
73
|
+
# 4. Playwright 渲染 HTML → PDF
|
74
|
+
asyncio.run(_html_to_pdf(html_path, tmp_pdf))
|
75
|
+
|
76
|
+
# 5. PyMuPDF 添加书签
|
77
|
+
_add_bookmarks(tmp_pdf, output_pdf, toc)
|
78
|
+
|
79
|
+
# 6. 清理
|
80
|
+
os.unlink(html_path)
|
81
|
+
os.unlink(tmp_pdf)
|
82
|
+
|
83
|
+
from pathlib import Path
|
84
|
+
full_path = Path(output_pdf)
|
85
|
+
# 提取文件名
|
86
|
+
filename = full_path.name # 'report.pdf'
|
87
|
+
# 提取路径
|
88
|
+
directory = full_path.parent # PosixPath('/Users/peter/Documents')
|
89
|
+
|
90
|
+
print(f"✅ {filename} is created with TOC")
|
91
|
+
print(f"✅ It is in {directory}")
|
92
|
+
|
93
|
+
#return output_pdf
|
94
|
+
return
|
95
|
+
|
96
|
+
#==============================================================================
|
97
|
+
"""
|
98
|
+
# 异步版本1
|
99
|
+
async def _html_to_pdf(html_path: str, pdf_path: str):
|
100
|
+
async with async_playwright() as p:
|
101
|
+
browser = await p.chromium.launch()
|
102
|
+
page = await browser.new_page()
|
103
|
+
await page.goto(f"file://{html_path}")
|
104
|
+
await page.pdf(
|
105
|
+
path=pdf_path,
|
106
|
+
#format="A4",
|
107
|
+
format="A3",
|
108
|
+
print_background=True,
|
109
|
+
margin={"top":"20mm","bottom":"20mm","left":"20mm","right":"20mm"},
|
110
|
+
)
|
111
|
+
await browser.close()
|
112
|
+
"""
|
113
|
+
import nest_asyncio
|
114
|
+
import asyncio
|
115
|
+
from playwright.async_api import async_playwright
|
116
|
+
|
117
|
+
nest_asyncio.apply()
|
118
|
+
|
119
|
+
async def _html_to_pdf(html_path: str, pdf_path: str):
|
120
|
+
async with async_playwright() as p:
|
121
|
+
browser = await p.chromium.launch()
|
122
|
+
page = await browser.new_page()
|
123
|
+
await page.goto(f"file://{html_path}")
|
124
|
+
await page.pdf(
|
125
|
+
path=pdf_path,
|
126
|
+
format="A3",
|
127
|
+
print_background=True,
|
128
|
+
margin={"top": "20mm", "bottom": "20mm", "left": "20mm", "right": "20mm"},
|
129
|
+
)
|
130
|
+
await browser.close()
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
"""
|
135
|
+
# 同步版本:不能在Jupyter中使用
|
136
|
+
from playwright.sync_api import sync_playwright
|
137
|
+
|
138
|
+
def _html_to_pdf(html_path: str, pdf_path: str):
|
139
|
+
with sync_playwright() as p:
|
140
|
+
browser = p.chromium.launch()
|
141
|
+
page = browser.new_page()
|
142
|
+
page.goto(f"file://{html_path}")
|
143
|
+
page.pdf(
|
144
|
+
path=pdf_path,
|
145
|
+
format="A3",
|
146
|
+
print_background=True,
|
147
|
+
margin={"top": "20mm", "bottom": "20mm", "left": "20mm", "right": "20mm"},
|
148
|
+
)
|
149
|
+
browser.close()
|
150
|
+
"""
|
151
|
+
#==============================================================================
|
152
|
+
|
153
|
+
def _extract_toc(nb_node) -> list[tuple[int,str]]:
|
154
|
+
"""
|
155
|
+
从每个 markdown 单元首行提取 # 级别和标题文本,
|
156
|
+
返回 [(level, title), …]
|
157
|
+
"""
|
158
|
+
toc = []
|
159
|
+
for cell in nb_node.cells:
|
160
|
+
if cell.cell_type != "markdown":
|
161
|
+
continue
|
162
|
+
first = cell.source.strip().splitlines()[0]
|
163
|
+
m = re.match(r"^(#{1,6})\s+(.*)", first)
|
164
|
+
if m:
|
165
|
+
toc.append((len(m.group(1)), m.group(2).strip()))
|
166
|
+
return toc
|
167
|
+
|
168
|
+
def _add_bookmarks(input_pdf: str, output_pdf: str, toc: list[tuple[int,str]]):
|
169
|
+
"""
|
170
|
+
用 PyMuPDF 打开临时 PDF,按 toc 列表查找页码,
|
171
|
+
然后用 set_toc() 批量写入书签。
|
172
|
+
"""
|
173
|
+
doc = fitz.open(input_pdf)
|
174
|
+
outline = []
|
175
|
+
for level, title in toc:
|
176
|
+
page_num = 1
|
177
|
+
# 搜索标题出现在第几页(0-based → +1)
|
178
|
+
for i in range(doc.page_count):
|
179
|
+
if title in doc.load_page(i).get_text():
|
180
|
+
page_num = i + 1
|
181
|
+
break
|
182
|
+
outline.append([level, title, page_num])
|
183
|
+
|
184
|
+
# 批量设置目录书签
|
185
|
+
doc.set_toc(outline)
|
186
|
+
doc.save(output_pdf)
|
187
|
+
|
188
|
+
# 使用示例(另起一个 cell 运行):
|
189
|
+
# ipynb = globals().get("__session__")
|
190
|
+
# ipynb2pdf(ipynb)
|
191
|
+
|
192
|
+
|
193
|
+
#==============================================================================
|
194
|
+
|
195
|
+
#==============================================================================
|
196
|
+
#==============================================================================
|
197
|
+
#==============================================================================
|
198
|
+
#==============================================================================
|
199
|
+
#==============================================================================
|
200
|
+
#==============================================================================
|
201
|
+
#==============================================================================
|
202
|
+
#==============================================================================
|
siat/save2pdf.py
CHANGED
@@ -19,37 +19,81 @@ SIAT:Security Investment Analysis Tool
|
|
19
19
|
# !pip install nbformat nbconvert playwright pymupdf nest_asyncio
|
20
20
|
# !playwright install
|
21
21
|
|
22
|
+
#关闭所有警告
|
23
|
+
import warnings; warnings.filterwarnings('ignore')
|
24
|
+
|
25
|
+
# 能够在Python 3.13下运行了!
|
22
26
|
import os
|
23
27
|
import re
|
28
|
+
import sys
|
24
29
|
import tempfile
|
25
|
-
import
|
26
|
-
|
27
|
-
import nest_asyncio
|
30
|
+
import subprocess
|
28
31
|
import nbformat
|
29
32
|
from nbconvert import HTMLExporter
|
30
|
-
|
31
|
-
import
|
33
|
+
import fitz # PyMuPDF
|
34
|
+
from pathlib import Path
|
32
35
|
|
33
|
-
|
36
|
+
import contextlib
|
37
|
+
import io
|
34
38
|
|
39
|
+
import time
|
40
|
+
from IPython.display import display, Javascript
|
41
|
+
|
42
|
+
# —— 新增:Notebook 强制保存 ——
|
43
|
+
def _save_current_notebook():
|
44
|
+
"""
|
45
|
+
在浏览器端触发一次保存:兼容 Classic Notebook、Lab 3.x/4.x。
|
46
|
+
"""
|
47
|
+
js = """
|
48
|
+
(function() {
|
49
|
+
// Classic Notebook
|
50
|
+
if (window.Jupyter && Jupyter.notebook) {
|
51
|
+
Jupyter.notebook.save_checkpoint();
|
52
|
+
}
|
53
|
+
// JupyterLab >=3: 用 app.commands
|
54
|
+
else if (window.jupyterapp && jupyterapp.commands) {
|
55
|
+
jupyterapp.commands.execute('docmanager:save');
|
56
|
+
}
|
57
|
+
// JupyterLab <=2 或其他
|
58
|
+
else if (window.require) {
|
59
|
+
require(['@jupyterlab/docmanager'], function(docManager) {
|
60
|
+
docManager.save();
|
61
|
+
});
|
62
|
+
}
|
63
|
+
})();
|
64
|
+
"""
|
65
|
+
try:
|
66
|
+
display(Javascript(js))
|
67
|
+
time.sleep(0.5) # 给浏览器一点时间写盘
|
68
|
+
except Exception:
|
69
|
+
pass
|
70
|
+
|
71
|
+
|
35
72
|
def ipynb2pdf(ipynb_path: str) -> str:
|
36
73
|
"""
|
37
74
|
将 .ipynb 转为带可跳转目录书签的 PDF。
|
38
75
|
返回生成的 PDF 文件路径。
|
39
76
|
"""
|
77
|
+
# 0. 强制保存当前 Notebook,貌似不管用,还是需要手动保存当前的Notebook
|
78
|
+
#print("Saving current ipynb ...")
|
79
|
+
#_save_current_notebook()
|
80
|
+
|
40
81
|
if not os.path.isfile(ipynb_path):
|
41
82
|
raise FileNotFoundError(f"找不到文件:{ipynb_path}")
|
42
83
|
output_pdf = ipynb_path[:-6] + ".pdf"
|
43
84
|
|
44
|
-
print(
|
85
|
+
print("Converting to PDF ...")
|
45
86
|
|
46
|
-
# 1. 读 notebook →
|
87
|
+
# 1. 读 notebook → 提取目录
|
47
88
|
nb = nbformat.read(ipynb_path, as_version=4)
|
48
89
|
toc = _extract_toc(nb)
|
49
90
|
|
50
|
-
# 2. nb → HTML
|
91
|
+
# 2. nb → HTML(同时关闭图 alt 检查错误信息)
|
51
92
|
exporter = HTMLExporter()
|
52
|
-
|
93
|
+
buf = io.StringIO()
|
94
|
+
# 屏蔽 stderr
|
95
|
+
with contextlib.redirect_stderr(buf):
|
96
|
+
html_body, _ = exporter.from_notebook_node(nb)
|
53
97
|
|
54
98
|
# 3. 临时写 HTML / PDF
|
55
99
|
with tempfile.NamedTemporaryFile("w", suffix=".html", encoding="utf-8", delete=False) as th:
|
@@ -58,87 +102,43 @@ def ipynb2pdf(ipynb_path: str) -> str:
|
|
58
102
|
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tp:
|
59
103
|
tmp_pdf = tp.name
|
60
104
|
|
61
|
-
# 4. Playwright 渲染 HTML → PDF
|
62
|
-
|
105
|
+
# 4. Playwright 渲染 HTML → PDF(在子进程中调用 sync API,避开 Jupyter 的 asyncio loop)
|
106
|
+
script = f"""
|
107
|
+
import sys
|
108
|
+
from playwright.sync_api import sync_playwright
|
109
|
+
|
110
|
+
p = sync_playwright().start()
|
111
|
+
browser = p.chromium.launch()
|
112
|
+
page = browser.new_page()
|
113
|
+
page.goto(r"file://{html_path}")
|
114
|
+
page.pdf(
|
115
|
+
path=r"{tmp_pdf}",
|
116
|
+
format="A3",
|
117
|
+
print_background=True,
|
118
|
+
margin={{"top":"20mm","bottom":"20mm","left":"20mm","right":"20mm"}}
|
119
|
+
)
|
120
|
+
browser.close()
|
121
|
+
p.stop()
|
122
|
+
"""
|
123
|
+
subprocess.run([sys.executable, "-c", script], check=True)
|
63
124
|
|
64
125
|
# 5. PyMuPDF 添加书签
|
65
126
|
_add_bookmarks(tmp_pdf, output_pdf, toc)
|
66
127
|
|
67
|
-
# 6.
|
128
|
+
# 6. 清理临时文件
|
68
129
|
os.unlink(html_path)
|
69
130
|
os.unlink(tmp_pdf)
|
70
131
|
|
71
|
-
|
132
|
+
# 打印结果
|
72
133
|
full_path = Path(output_pdf)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
directory = full_path.parent # PosixPath('/Users/peter/Documents')
|
77
|
-
|
78
|
-
print(f"✅ {filename} is created with TOC")
|
79
|
-
print(f"✅ It is in {directory}")
|
80
|
-
|
134
|
+
print(f"✅ {full_path.name} is created with TOC")
|
135
|
+
print(f"✅ It is in {full_path.parent}")
|
136
|
+
|
81
137
|
#return output_pdf
|
82
138
|
return
|
83
139
|
|
84
|
-
#==============================================================================
|
85
|
-
"""
|
86
|
-
# 异步版本1
|
87
|
-
async def _html_to_pdf(html_path: str, pdf_path: str):
|
88
|
-
async with async_playwright() as p:
|
89
|
-
browser = await p.chromium.launch()
|
90
|
-
page = await browser.new_page()
|
91
|
-
await page.goto(f"file://{html_path}")
|
92
|
-
await page.pdf(
|
93
|
-
path=pdf_path,
|
94
|
-
#format="A4",
|
95
|
-
format="A3",
|
96
|
-
print_background=True,
|
97
|
-
margin={"top":"20mm","bottom":"20mm","left":"20mm","right":"20mm"},
|
98
|
-
)
|
99
|
-
await browser.close()
|
100
|
-
"""
|
101
|
-
import nest_asyncio
|
102
|
-
import asyncio
|
103
|
-
from playwright.async_api import async_playwright
|
104
|
-
|
105
|
-
nest_asyncio.apply()
|
106
|
-
|
107
|
-
async def _html_to_pdf(html_path: str, pdf_path: str):
|
108
|
-
async with async_playwright() as p:
|
109
|
-
browser = await p.chromium.launch()
|
110
|
-
page = await browser.new_page()
|
111
|
-
await page.goto(f"file://{html_path}")
|
112
|
-
await page.pdf(
|
113
|
-
path=pdf_path,
|
114
|
-
format="A3",
|
115
|
-
print_background=True,
|
116
|
-
margin={"top": "20mm", "bottom": "20mm", "left": "20mm", "right": "20mm"},
|
117
|
-
)
|
118
|
-
await browser.close()
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
"""
|
123
|
-
# 同步版本:不能在Jupyter中使用
|
124
|
-
from playwright.sync_api import sync_playwright
|
125
|
-
|
126
|
-
def _html_to_pdf(html_path: str, pdf_path: str):
|
127
|
-
with sync_playwright() as p:
|
128
|
-
browser = p.chromium.launch()
|
129
|
-
page = browser.new_page()
|
130
|
-
page.goto(f"file://{html_path}")
|
131
|
-
page.pdf(
|
132
|
-
path=pdf_path,
|
133
|
-
format="A3",
|
134
|
-
print_background=True,
|
135
|
-
margin={"top": "20mm", "bottom": "20mm", "left": "20mm", "right": "20mm"},
|
136
|
-
)
|
137
|
-
browser.close()
|
138
|
-
"""
|
139
|
-
#==============================================================================
|
140
140
|
|
141
|
-
def _extract_toc(nb_node) -> list[tuple[int,str]]:
|
141
|
+
def _extract_toc(nb_node) -> list[tuple[int, str]]:
|
142
142
|
"""
|
143
143
|
从每个 markdown 单元首行提取 # 级别和标题文本,
|
144
144
|
返回 [(level, title), …]
|
@@ -153,7 +153,8 @@ def _extract_toc(nb_node) -> list[tuple[int,str]]:
|
|
153
153
|
toc.append((len(m.group(1)), m.group(2).strip()))
|
154
154
|
return toc
|
155
155
|
|
156
|
-
|
156
|
+
|
157
|
+
def _add_bookmarks(input_pdf: str, output_pdf: str, toc: list[tuple[int, str]]):
|
157
158
|
"""
|
158
159
|
用 PyMuPDF 打开临时 PDF,按 toc 列表查找页码,
|
159
160
|
然后用 set_toc() 批量写入书签。
|
@@ -161,18 +162,18 @@ def _add_bookmarks(input_pdf: str, output_pdf: str, toc: list[tuple[int,str]]):
|
|
161
162
|
doc = fitz.open(input_pdf)
|
162
163
|
outline = []
|
163
164
|
for level, title in toc:
|
164
|
-
page_num = 1
|
165
165
|
# 搜索标题出现在第几页(0-based → +1)
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
166
|
+
page_num = next(
|
167
|
+
(i+1 for i in range(doc.page_count)
|
168
|
+
if title in doc.load_page(i).get_text()),
|
169
|
+
1
|
170
|
+
)
|
170
171
|
outline.append([level, title, page_num])
|
171
172
|
|
172
|
-
# 批量设置目录书签
|
173
173
|
doc.set_toc(outline)
|
174
174
|
doc.save(output_pdf)
|
175
175
|
|
176
|
+
|
176
177
|
# 使用示例(另起一个 cell 运行):
|
177
178
|
# ipynb = globals().get("__session__")
|
178
179
|
# ipynb2pdf(ipynb)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: siat
|
3
|
-
Version: 3.11.
|
3
|
+
Version: 3.11.6
|
4
4
|
Summary: Securities Investment Analysis Tools (siat)
|
5
5
|
Home-page: https://pypi.org/project/siat/
|
6
6
|
Author: Prof. WANG Dehong, International Business School, Beijing Foreign Studies University
|
@@ -11,7 +11,7 @@ Project-URL: Homepage, https://pypi.org/project/siat/
|
|
11
11
|
Keywords: investment,finance,technical analysis,siat
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
13
13
|
Classifier: Operating System :: OS Independent
|
14
|
-
Requires-Python:
|
14
|
+
Requires-Python: >=3.7
|
15
15
|
Description-Content-Type: text/markdown
|
16
16
|
License-File: LICENSE
|
17
17
|
Requires-Dist: pandas-datareader
|
@@ -45,7 +45,7 @@ Requires-Dist: ipywidgets
|
|
45
45
|
Requires-Dist: yahooquery
|
46
46
|
Requires-Dist: alpha-vantage
|
47
47
|
Requires-Dist: tiingo[pandas]
|
48
|
-
Requires-Dist: numpy<2
|
48
|
+
Requires-Dist: numpy <2
|
49
49
|
Requires-Dist: playwright
|
50
50
|
Requires-Dist: pymupdf
|
51
51
|
Requires-Dist: pypandoc
|
@@ -10,7 +10,7 @@ siat/bond_china.py,sha256=WzUhjYYk8tsr3BDWLQcpuj9DqNxTzBSIi_wuAOZ48kY,3082
|
|
10
10
|
siat/bond_zh_sina.py,sha256=26BohGcS120utwqg9dJvdGm5OkuNpNu5bco80uOuQpU,4423
|
11
11
|
siat/capm_beta.py,sha256=t8-xr90II0JzbjsTOZNpRze_mKTvBRXjwN2o0N0tgD8,30521
|
12
12
|
siat/capm_beta2.py,sha256=S2x6PrWp_1FyzVmG2MVzCf7LlpfHHEJxroJH2b26DvQ,35989
|
13
|
-
siat/common.py,sha256=
|
13
|
+
siat/common.py,sha256=L8ZAz0gkJjatm3CQ5mwJTPpATQtbagUPwI-7i3O5p18,198444
|
14
14
|
siat/compare_cross.py,sha256=3iP9TH2h3w27F2ARZc7FjKcErYCzWRc-TPiymOyoVtw,24171
|
15
15
|
siat/copyrights.py,sha256=YMLjZb328YpFMR-s_GUu0HBgeGce3pV7DgRut8S3I7w,690
|
16
16
|
siat/cryptocurrency.py,sha256=QSc4jK9VFlqBWVu-0th1BIMt8wC-5R5sWky3EaNupy0,27940
|
@@ -50,10 +50,12 @@ siat/risk_adjusted_return.py,sha256=Q4ZRdTF57eNt4QCjeQ7uA8nG56Jls8f_QfJasZQEo0M,
|
|
50
50
|
siat/risk_adjusted_return2.py,sha256=gCtHhfGNlV1wHqU9gfHJ_n17wRSyTMxc7lS8jgZ-GQk,87409
|
51
51
|
siat/risk_evaluation.py,sha256=xfgLSKlIWYmRJrIL4kn2k2hp9fyOMAzYGIhi9ImvKOw,88917
|
52
52
|
siat/risk_free_rate.py,sha256=IBuRqA2kppdZsW4D4fapW7vnM5HMEXOn95A5r9Pkwlo,12384
|
53
|
-
siat/save2docx.py,sha256=
|
53
|
+
siat/save2docx.py,sha256=MhgZamjMYPHIVxNDXP0XGKaYn5qEePdSqMSLQRWMH10,13855
|
54
|
+
siat/save2pdf-20250716.py,sha256=BZ8h1YZFYFwCD15a2VcydMmMmfsbA-8WzeDJQr_or3g,6400
|
54
55
|
siat/save2pdf-playwright-20250712.py,sha256=cB1L5lH2n6RfgubCLFR7a617OGnrtT9IQhWXWGncoFs,5114
|
56
|
+
siat/save2pdf-playwright-20250714.py,sha256=WPdjT4kjXiAoWri-nyvNlPhvjmQlAHX80qmIT5GGYxs,6785
|
55
57
|
siat/save2pdf-weasyprint-20250712.py,sha256=ZVZq5yT-grcmdY3qq8XXZ7OCDCGqvh66o2WfszoK9ws,4570
|
56
|
-
siat/save2pdf.py,sha256=
|
58
|
+
siat/save2pdf.py,sha256=mBv3A_Ly9sx53VaGaMW-Hy4haeLuKo10Y2iMRVv1r70,6195
|
57
59
|
siat/sector_china.py,sha256=uLsDXdRBDVfgG6tnXWnQOTyDmyZfglVO9DRUYU2e3pk,157914
|
58
60
|
siat/security_price2.py,sha256=DDiZ2dlv_TYPLhA8-gGb9i9xrl88r4rgSMEcxqQ6aU0,28065
|
59
61
|
siat/security_prices.py,sha256=X3ip0q_m3OL3QRNRkr_lYQk-wsXLf6dWkFkyoZijhck,129368
|
@@ -75,8 +77,8 @@ siat/valuation.py,sha256=xGizcKJZ3ADLWWHm2TFQub18FxiDv2doQwBwbEqyqz0,51980
|
|
75
77
|
siat/valuation_china.py,sha256=eSKIDckyjG8QkENlW_OKkqbQHno8pzDcomBO9iGNJVM,83079
|
76
78
|
siat/var_model_validation.py,sha256=loqziBYO2p0xkeWm3Rb1rJsDhbcgAZ5aR9rBLRwLU5E,17624
|
77
79
|
siat/yf_name.py,sha256=laNKMTZ9hdenGX3IZ7G0a2RLBKEWtUQJFY9CWuk_fp8,24058
|
78
|
-
siat-3.11.
|
79
|
-
siat-3.11.
|
80
|
-
siat-3.11.
|
81
|
-
siat-3.11.
|
82
|
-
siat-3.11.
|
80
|
+
siat-3.11.6.dist-info/LICENSE,sha256=NTEMMROY9_4U1szoKC3N2BLHcDd_o5uTgqdVH8tbApw,1071
|
81
|
+
siat-3.11.6.dist-info/METADATA,sha256=xJppCaG6hxV_JMVxHQjN7jfiNQRAH4-wfdtZQWlHRxA,8588
|
82
|
+
siat-3.11.6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
83
|
+
siat-3.11.6.dist-info/top_level.txt,sha256=X5R8wrVviq8agwJFVRVDsufkuOJuit-1qAT_kXeptrY,17
|
84
|
+
siat-3.11.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|