threadlepy 0.0.1__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.
File without changes
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: threadlepy
3
+ Version: 0.0.1
4
+ Summary: A small example package
5
+ Author-email: Example Author <author@example.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/pypa/sampleproject
8
+ Project-URL: Issues, https://github.com/pypa/sampleproject/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # Example Package
17
+
18
+ This is a simple example package. You can use
19
+ [GitHub-flavored Markdown](https://guides.github.com/features/mastering-markdown/)
20
+ to write your content.
@@ -0,0 +1,5 @@
1
+ # Example Package
2
+
3
+ This is a simple example package. You can use
4
+ [GitHub-flavored Markdown](https://guides.github.com/features/mastering-markdown/)
5
+ to write your content.
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "threadlepy"
3
+ version = "0.0.1"
4
+ authors = [
5
+ { name="Example Author", email="author@example.com" },
6
+ ]
7
+ description = "A small example package"
8
+ readme = "README.md"
9
+ requires-python = ">=3.9"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "Operating System :: OS Independent",
13
+ ]
14
+ license = "MIT"
15
+ license-files = ["LICEN[CS]E*"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/pypa/sampleproject"
19
+ Issues = "https://github.com/pypa/sampleproject/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import signal
5
+ import subprocess
6
+ import shutil
7
+ import json
8
+ import time
9
+ import re
10
+ import select
11
+ from dataclasses import dataclass
12
+ from typing import Any, Dict, Optional, Union
13
+
14
+ @dataclass(frozen=True)
15
+ class ThreadleStruct:
16
+ name: str
17
+
18
+ ThreadleName = Union[str, ThreadleStruct]
19
+
20
+ proc: Optional[subprocess.Popen[str]] = None
21
+
22
+ def stop():
23
+ global proc
24
+ if proc and proc.poll() is None:
25
+ print("threadle stopped")
26
+ print(f"threadle {os.getpgid(proc.pid)} killed")
27
+ os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
28
+ proc = None
29
+
30
+ def start(path: str | None = None) -> subprocess.Popen[str]:
31
+ global proc
32
+ exe = path or shutil.which("threadle")
33
+
34
+ if not exe:
35
+ raise FileNotFoundError("Threadle path not found.")
36
+ exe = os.path.abspath(exe)
37
+
38
+ if proc and proc.poll() is None:
39
+ raise RuntimeError("Threadle already started.")
40
+
41
+ proc = subprocess.Popen(
42
+ [exe, "--json", "--silent"],
43
+ stdin=subprocess.PIPE,
44
+ stdout=subprocess.PIPE,
45
+ stderr=subprocess.DEVNULL,
46
+ text=True,
47
+ bufsize=1,
48
+ start_new_session=True,
49
+ )
50
+ print(f"threadle started pid={proc.pid} pgid={os.getpgid(proc.pid)}")
51
+ return proc
52
+
53
+ def collect_args(**kwargs):
54
+ return {k: v for k, v in kwargs.items() if v is not None}
55
+
56
+ def json_cmd(command: str, args: Optional[Dict[str, Any]] = None, assign: Optional[str] = None) -> str:
57
+ dto = {
58
+ "Assign": assign, # None -> JSON null
59
+ "Command": str(command),
60
+ "Args": args or {}, # 关键:保证是 {}
61
+ }
62
+ return json.dumps(dto, ensure_ascii=False)
63
+
64
+ def send_command(cmd_json: str, timeout: float = 30.0) -> Dict[str, Any]:
65
+ global proc
66
+ if proc is None or proc.poll() is not None:
67
+ raise RuntimeError("Threadle process is not running.")
68
+ assert proc.stdin is not None
69
+ assert proc.stdout is not None
70
+
71
+ proc.stdin.write(cmd_json.rstrip("\n") + "\n")
72
+ proc.stdin.flush()
73
+
74
+ out_lines: list[str] = []
75
+ end = time.time() + timeout
76
+
77
+ while time.time() < end:
78
+ r, _, _ = select.select([proc.stdout], [], [], 0.01)
79
+ if not r:
80
+ continue
81
+
82
+ line = proc.stdout.readline()
83
+ if line == "":
84
+ raise RuntimeError("EOF from Threadle.")
85
+
86
+ out_lines.append(line)
87
+
88
+ s = re.sub(r"^\s*>\s*", "", line).strip()
89
+
90
+ if s.startswith("{") and s.endswith("}"):
91
+ try:
92
+ resp = json.loads(s)
93
+ if isinstance(resp, dict):
94
+ return resp
95
+ except json.JSONDecodeError:
96
+ pass
97
+
98
+ raise TimeoutError("Timed out waiting for JSON response.")
99
+
100
+ def unwrap(resp: Dict[str, Any], *, print_message: bool = True) -> Any:
101
+ if resp.get("Success") is not True:
102
+ code = resp.get("Code") or "Error"
103
+ msg = resp.get("Message") or "Threadle error"
104
+ raise RuntimeError(f"[{code}] {msg}")
105
+
106
+ if print_message:
107
+ msg = resp.get("Message")
108
+ if isinstance(msg, str) and msg.strip():
109
+ print(msg)
110
+
111
+ return resp.get("Payload")
112
+
113
+ def _norm(v: Any) -> Any:
114
+ if isinstance(v, ThreadleStruct):
115
+ return v.name
116
+ if isinstance(v, bool):
117
+ return "true" if v else "false"
118
+ if isinstance(v, (int, float)):
119
+ return str(v)
120
+ if isinstance(v, (list, tuple)):
121
+ return ";".join(str(x) for x in v)
122
+ return v
123
+
124
+ def call(cmd: str, locals_dict: dict, *, assign: Optional[str] = None, timeout: float = 30.0) -> Any:
125
+ args = collect_args(**locals_dict)
126
+ args.pop("cmd", None)
127
+ args.pop("assign", None)
128
+
129
+ if assign is not None:
130
+ for k, v in list(args.items()):
131
+ if v == assign:
132
+ args.pop(k, None)
133
+ break
134
+
135
+ args = {k: _norm(v) for k, v in args.items()}
136
+ cmd_json = json_cmd(cmd, args=args, assign=assign)
137
+ resp = send_command(cmd_json, timeout=timeout)
138
+ return unwrap(resp)
@@ -0,0 +1,372 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Optional, Union
3
+
4
+ from .client import call, ThreadleStruct, ThreadleName
5
+
6
+ NodeId = Union[int, str]
7
+
8
+
9
+ # --------------------
10
+ # Workdir
11
+ # --------------------
12
+ def get_workdir():
13
+ cmd = "getwd"
14
+ assign = None
15
+ return call(cmd, locals(), assign=assign)
16
+
17
+ def set_workdir(dir: str):
18
+ cmd = "setwd"
19
+ assign = None
20
+ return call(cmd, locals(), assign=assign)
21
+
22
+ # --------------------
23
+ # Meta
24
+ # --------------------
25
+ def inventory():
26
+ cmd = "i"
27
+ assign = None
28
+ return call(cmd, locals(), assign=assign)
29
+
30
+ def info(structure: Union[str, ThreadleStruct]):
31
+ cmd = "info"
32
+ assign = None
33
+ return call(cmd, locals(), assign=assign)
34
+
35
+ # ============================================
36
+ # Layer / Edge / Hyperedge / Affiliation ops
37
+ # ============================================
38
+
39
+ def add_aff(network: ThreadleName, layername: str, nodeid: NodeId, hypername: str,
40
+ addmissingnode: bool = True, addmissingaffiliation: bool = True):
41
+ cmd = "addaff"
42
+ assign = None
43
+ return call(cmd, locals(), assign=assign)
44
+
45
+
46
+ def remove_aff(network: ThreadleName, layername: str, nodeid: NodeId, hypername: str):
47
+ cmd = "removeaff"
48
+ assign = None
49
+
50
+ return call(cmd, locals(), assign=assign)
51
+
52
+
53
+ def add_edge(network: ThreadleName, layername: str, node1id: NodeId, node2id: NodeId,
54
+ value: int = 1, addmissingnodes: bool = True):
55
+ cmd = "addedge"
56
+ assign = None
57
+ return call(cmd, locals(), assign=assign)
58
+
59
+
60
+ def remove_edge(network: ThreadleName, layername: str, node1id: NodeId, node2id: NodeId):
61
+ cmd = "removeedge"
62
+ assign = None
63
+ return call(cmd, locals(), assign=assign)
64
+
65
+
66
+ def check_edge(network: ThreadleName, layername: str, node1id: NodeId, node2id: NodeId):
67
+ cmd = "checkedge"
68
+ assign = None
69
+ return call(cmd, locals(), assign=assign)
70
+
71
+
72
+ def clear_layer(network: ThreadleName, layername: str):
73
+ cmd = "clearlayer"
74
+ assign = None
75
+ return call(cmd, locals(), assign=assign)
76
+
77
+
78
+ def add_hyper(network: ThreadleName, layername: str, hypername: str,
79
+ nodes: Optional[list[Any]] = None, addmissingnodes: bool = True):
80
+ """
81
+ Notes:
82
+ - ThreadleR collapses node vectors with ';' and uses "" when None.
83
+ """
84
+ cmd = "addhyper"
85
+ assign = None
86
+ nodes = "" if nodes is None else ";".join(map(str, nodes))
87
+ return call(cmd, locals(), assign=assign)
88
+
89
+
90
+ def remove_hyper(network: ThreadleName, layername: str, hypername: str):
91
+ cmd = "removehyper"
92
+ assign = None
93
+ return call(cmd, locals(), assign=assign)
94
+
95
+
96
+ def add_layer(network: ThreadleName, layername: str, mode: int,
97
+ directed: bool = False, valuetype: str = "binary", selfties: bool = False):
98
+ """
99
+ valuetype: "binary" or "valued"
100
+ """
101
+ if valuetype not in ("binary", "valued"):
102
+ raise ValueError('valuetype must be "binary" or "valued".')
103
+ cmd = "addlayer"
104
+ assign = None
105
+ return call(cmd, locals(), assign=assign)
106
+
107
+ def remove_layer(network: Union[str, ThreadleStruct], layername: str):
108
+ cmd = "removelayer"
109
+ assign = None
110
+ return call(cmd, locals(), assign=assign)
111
+
112
+ # ============================================
113
+ # Nodeset / Node ops
114
+ # ============================================
115
+
116
+ def add_node(structure: ThreadleName, id: NodeId):
117
+ cmd = "addnode"
118
+ assign = None
119
+ return call(cmd, locals(), assign=assign)
120
+
121
+
122
+ def remove_node(structure: ThreadleName, nodeid: NodeId):
123
+ cmd = "removenode"
124
+ assign = None
125
+ return call(cmd, locals(), assign=assign)
126
+
127
+
128
+ def get_nbr_nodes(structure: ThreadleName):
129
+ cmd = "getnbrnodes"
130
+ assign = None
131
+ return call(cmd, locals(), assign=assign)
132
+
133
+
134
+ def get_nodeid_by_index(structure: ThreadleName, index: int):
135
+ cmd = "getnodeidbyindex"
136
+ assign = None
137
+ return call(cmd, locals(), assign=assign)
138
+
139
+
140
+ def get_random_node(structure: ThreadleName):
141
+ cmd = "getrandomnode"
142
+ assign = None
143
+ return call(cmd, locals(), assign=assign)
144
+
145
+
146
+ # ============================================
147
+ # Attributes
148
+ # ============================================
149
+
150
+ def define_attr(structure: ThreadleName, attrname: str, attrtype: str = "int"):
151
+ """
152
+ attrtype: "int", "char", "float", or "bool"
153
+ """
154
+ if attrtype not in ("int", "char", "float", "bool"):
155
+ raise ValueError('attrtype must be one of: "int", "char", "float", "bool".')
156
+ cmd = "defineattr"
157
+ assign = None
158
+ return call(cmd, locals(), assign=assign)
159
+
160
+
161
+ def undefine_attr(structure: ThreadleName, attrname: str):
162
+ cmd = "undefineattr"
163
+ assign = None
164
+ return call(cmd, locals(), assign=assign)
165
+
166
+
167
+ def get_attr(structure: ThreadleName, nodeid: NodeId, attrname: str):
168
+ cmd = "getattr"
169
+ assign = None
170
+ return call(cmd, locals(), assign=assign)
171
+
172
+
173
+ def set_attr(structure: ThreadleName, nodeid: NodeId, attrname: str, attrvalue: Any):
174
+ cmd = "setattr"
175
+ assign = None
176
+ return call(cmd, locals(), assign=assign)
177
+
178
+
179
+ def remove_attr(structure: ThreadleName, nodeid: NodeId, attrname: str):
180
+ cmd = "removeattr"
181
+ assign = None
182
+ return call(cmd, locals(), assign=assign)
183
+
184
+
185
+ # ============================================
186
+ # Edge queries / paths / neighborhoods
187
+ # ============================================
188
+
189
+ def get_edge(network: ThreadleName, layername: str, node1id: NodeId, node2id: NodeId):
190
+ cmd = "getedge"
191
+ assign = None
192
+ return call(cmd, locals(), assign=assign)
193
+
194
+
195
+ def get_node_alters(network: ThreadleName, nodeid: NodeId, layername: str = "",
196
+ direction: str = "both", unique: bool = False):
197
+ """
198
+ direction: "both", "in", "out"
199
+ layername:
200
+ - "" for all layers
201
+ - if you pass multiple layer names, pre-join with ';' before calling this wrapper
202
+ """
203
+ if direction not in ("both", "in", "out"):
204
+ raise ValueError('direction must be one of: "both", "in", "out".')
205
+ cmd = "getnodealters"
206
+ assign = None
207
+ layername = "" if (layername is None or layername == "") else layername
208
+ return call(cmd, locals(), assign=assign)
209
+
210
+
211
+ def get_random_alter(network: ThreadleName, nodeid: NodeId, layername: str = "",
212
+ direction: str = "both", balanced: bool = False):
213
+ if direction not in ("both", "in", "out"):
214
+ raise ValueError('direction must be one of: "both", "in", "out".')
215
+ cmd = "getrandomalter"
216
+ assign = None
217
+ return call(cmd, locals(), assign=assign)
218
+
219
+
220
+ def shortest_path(network: ThreadleName, node1id: NodeId, node2id: NodeId, layername: str = ""):
221
+ cmd = "shortestpath"
222
+ assign = None
223
+ layername = "" if layername is None else layername
224
+ return call(cmd, locals(), assign=assign)
225
+
226
+
227
+ # ============================================
228
+ # Network measures / transforms
229
+ # ============================================
230
+
231
+ def degree(network: ThreadleName, layername: str, attrname: Optional[str] = None, direction: str = "in"):
232
+ if direction not in ("in", "out", "both"):
233
+ raise ValueError('direction must be one of: "in", "out", "both".')
234
+ cmd = "degree"
235
+ assign = None
236
+ return call(cmd, locals(), assign=assign)
237
+
238
+
239
+ def density(network: ThreadleName, layername: str):
240
+ cmd = "density"
241
+ assign = None
242
+ return call(cmd, locals(), assign=assign)
243
+
244
+
245
+ def dichotomize(network: ThreadleName, layername: str,
246
+ cond: str = "ge", threshold: Any = 1,
247
+ truevalue: Any = 1, falsevalue: Any = 0,
248
+ newlayername: Optional[str] = None):
249
+ """
250
+ cond: "ge","eq","ne","gt","lt","le","isnull","notnull"
251
+ """
252
+ if cond not in ("ge", "eq", "ne", "gt", "lt", "le", "isnull", "notnull"):
253
+ raise ValueError('cond must be one of: ge, eq, ne, gt, lt, le, isnull, notnull')
254
+ cmd = "dichotomize"
255
+ assign = None
256
+ return call(cmd, locals(), assign=assign)
257
+
258
+
259
+ def components(network: ThreadleName, layname: str, attrname: str):
260
+ cmd = "components"
261
+ assign = None
262
+ return call(cmd, locals(), assign=assign)
263
+
264
+ def symmetrize(network: Union[str, ThreadleStruct], layername: str, newlayername: Optional[str] = None):
265
+ cmd = "symmetrize"
266
+ assign = None
267
+ return call(cmd, locals(), assign=assign)
268
+
269
+ # ============================================
270
+ # Creation / IO / lifecycle
271
+ # ============================================
272
+
273
+ def create_nodeset(name: str, createnodes: int = 0):
274
+ cmd = "createnodeset"
275
+ assign = name
276
+ call(cmd, locals(), assign=assign)
277
+ # Return a client-side handle (assumes you have Nodeset/ThreadleStruct-style wrappers)
278
+ return ThreadleStruct(name=name)
279
+
280
+
281
+ def create_network(nodeset: ThreadleName, name: str):
282
+ cmd = "createnetwork"
283
+ assign = name
284
+ call(cmd, locals(), assign=assign)
285
+ return ThreadleStruct(name=name)
286
+
287
+
288
+ def load_file(name: str, file: str, type: str):
289
+ """
290
+ type: "network" or "nodeset"
291
+ """
292
+ cmd = "loadfile"
293
+ assign = name
294
+ call(cmd, locals(), assign=assign)
295
+
296
+ # Return a client-side handle (replace with Network/Nodeset classes if you have them)
297
+ if type == "network":
298
+ return ThreadleStruct(name=name)
299
+ if type == "nodeset":
300
+ return ThreadleStruct(name=name)
301
+ raise ValueError(f"Unknown type: {type}")
302
+
303
+
304
+ def save_file(structure: ThreadleName, file: str = ""):
305
+ """
306
+ If file is empty, ThreadleR defaults to "<structure>.tsv".
307
+ """
308
+ cmd = "savefile"
309
+ assign = None
310
+ if not file:
311
+ file = f"{structure}.tsv"
312
+ return call(cmd, locals(), assign=assign)
313
+
314
+
315
+ def import_layer(network: ThreadleName, layername: str, file: str,
316
+ format: str = "edgelist", sep: str = "\t", addmissingnodes: bool = False):
317
+ """
318
+ format: "edgelist" or "matrix"
319
+ """
320
+ if format not in ("edgelist", "matrix"):
321
+ raise ValueError('format must be "edgelist" or "matrix".')
322
+ cmd = "importlayer"
323
+ assign = None
324
+ return call(cmd, locals(), assign=assign)
325
+
326
+
327
+ def delete(structure: ThreadleName):
328
+ cmd = "delete"
329
+ assign = None
330
+ return call(cmd, locals(), assign=assign)
331
+
332
+
333
+ def delete_all():
334
+ cmd = "deleteall"
335
+ assign = None
336
+ return call(cmd, locals(), assign=assign)
337
+
338
+
339
+ # ============================================
340
+ # Subsetting / filtering
341
+ # ============================================
342
+
343
+ def filter_nodeset(name: str, nodeset: ThreadleName, attrname: str, cond: str, attrvalue: Any):
344
+ cmd = "filter"
345
+ assign = name
346
+ call(cmd, locals(), assign=assign)
347
+ return ThreadleStruct(name=name)
348
+
349
+
350
+ def subnet(name: str, network: ThreadleName, nodeset: ThreadleName):
351
+ cmd = "subnet"
352
+ assign = name
353
+ call(cmd, locals(), assign=assign)
354
+ return ThreadleStruct(name=name)
355
+
356
+
357
+ # ============================================
358
+ # Random generation / settings
359
+ # ============================================
360
+
361
+ def generate(network: ThreadleName, layername: str, type: str,
362
+ p: Optional[float] = None, k: Optional[int] = None,
363
+ beta: Optional[float] = None, m: Optional[int] = None):
364
+ cmd = "generate"
365
+ assign = None
366
+ return call(cmd, locals(), assign=assign)
367
+
368
+
369
+ def setting(name: str, value: Any):
370
+ cmd = "setting"
371
+ assign = None
372
+ return call(cmd, locals(), assign=assign)
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: threadlepy
3
+ Version: 0.0.1
4
+ Summary: A small example package
5
+ Author-email: Example Author <author@example.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/pypa/sampleproject
8
+ Project-URL: Issues, https://github.com/pypa/sampleproject/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # Example Package
17
+
18
+ This is a simple example package. You can use
19
+ [GitHub-flavored Markdown](https://guides.github.com/features/mastering-markdown/)
20
+ to write your content.
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/threadlepy/__init__.py
5
+ src/threadlepy/client.py
6
+ src/threadlepy/commands.py
7
+ src/threadlepy.egg-info/PKG-INFO
8
+ src/threadlepy.egg-info/SOURCES.txt
9
+ src/threadlepy.egg-info/dependency_links.txt
10
+ src/threadlepy.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ threadlepy