actornet 0.1.0__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.
@@ -0,0 +1,418 @@
1
+ ## Ignore Visual Studio temporary files, build results, and
2
+ ## files generated by popular Visual Studio add-ons.
3
+ ##
4
+ ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5
+
6
+ # User-specific files
7
+ *.rsuser
8
+ *.suo
9
+ *.user
10
+ *.userosscache
11
+ *.sln.docstates
12
+ *.env
13
+
14
+ # User-specific files (MonoDevelop/Xamarin Studio)
15
+ *.userprefs
16
+
17
+ # Mono auto generated files
18
+ mono_crash.*
19
+
20
+ # Build results
21
+ [Dd]ebug/
22
+ [Dd]ebugPublic/
23
+ [Rr]elease/
24
+ [Rr]eleases/
25
+ x64/
26
+ x86/
27
+ [Ww][Ii][Nn]32/
28
+ [Aa][Rr][Mm]/
29
+ [Aa][Rr][Mm]64/
30
+ [Aa][Rr][Mm]64[Ee][Cc]/
31
+ bld/
32
+ [Oo]bj/
33
+ [Oo]ut/
34
+ [Ll]og/
35
+ [Ll]ogs/
36
+
37
+ # Build results on 'Bin' directories
38
+ **/[Bb]in/*
39
+ # Uncomment if you have tasks that rely on *.refresh files to move binaries
40
+ # (https://github.com/github/gitignore/pull/3736)
41
+ #!**/[Bb]in/*.refresh
42
+
43
+ # Visual Studio 2015/2017 cache/options directory
44
+ .vs/
45
+ # Uncomment if you have tasks that create the project's static files in wwwroot
46
+ #wwwroot/
47
+
48
+ # Visual Studio 2017 auto generated files
49
+ Generated\ Files/
50
+
51
+ # MSTest test Results
52
+ [Tt]est[Rr]esult*/
53
+ [Bb]uild[Ll]og.*
54
+ *.trx
55
+
56
+ # NUnit
57
+ *.VisualState.xml
58
+ TestResult.xml
59
+ nunit-*.xml
60
+
61
+ # Approval Tests result files
62
+ *.received.*
63
+
64
+ # Build Results of an ATL Project
65
+ [Dd]ebugPS/
66
+ [Rr]eleasePS/
67
+ dlldata.c
68
+
69
+ # Benchmark Results
70
+ BenchmarkDotNet.Artifacts/
71
+
72
+ # .NET Core
73
+ project.lock.json
74
+ project.fragment.lock.json
75
+ artifacts/
76
+
77
+ # ASP.NET Scaffolding
78
+ ScaffoldingReadMe.txt
79
+
80
+ # StyleCop
81
+ StyleCopReport.xml
82
+
83
+ # Files built by Visual Studio
84
+ *_i.c
85
+ *_p.c
86
+ *_h.h
87
+ *.ilk
88
+ *.meta
89
+ *.obj
90
+ *.idb
91
+ *.iobj
92
+ *.pch
93
+ *.pdb
94
+ *.ipdb
95
+ *.pgc
96
+ *.pgd
97
+ *.rsp
98
+ # but not Directory.Build.rsp, as it configures directory-level build defaults
99
+ !Directory.Build.rsp
100
+ *.sbr
101
+ *.tlb
102
+ *.tli
103
+ *.tlh
104
+ *.tmp
105
+ *.tmp_proj
106
+ *_wpftmp.csproj
107
+ *.log
108
+ *.tlog
109
+ *.vspscc
110
+ *.vssscc
111
+ .builds
112
+ *.pidb
113
+ *.svclog
114
+ *.scc
115
+
116
+ # Chutzpah Test files
117
+ _Chutzpah*
118
+
119
+ # Visual C++ cache files
120
+ ipch/
121
+ *.aps
122
+ *.ncb
123
+ *.opendb
124
+ *.opensdf
125
+ *.sdf
126
+ *.cachefile
127
+ *.VC.db
128
+ *.VC.VC.opendb
129
+
130
+ # Visual Studio profiler
131
+ *.psess
132
+ *.vsp
133
+ *.vspx
134
+ *.sap
135
+
136
+ # Visual Studio Trace Files
137
+ *.e2e
138
+
139
+ # TFS 2012 Local Workspace
140
+ $tf/
141
+
142
+ # Guidance Automation Toolkit
143
+ *.gpState
144
+
145
+ # ReSharper is a .NET coding add-in
146
+ _ReSharper*/
147
+ *.[Rr]e[Ss]harper
148
+ *.DotSettings.user
149
+
150
+ # TeamCity is a build add-in
151
+ _TeamCity*
152
+
153
+ # DotCover is a Code Coverage Tool
154
+ *.dotCover
155
+
156
+ # AxoCover is a Code Coverage Tool
157
+ .axoCover/*
158
+ !.axoCover/settings.json
159
+
160
+ # Coverlet is a free, cross platform Code Coverage Tool
161
+ coverage*.json
162
+ coverage*.xml
163
+ coverage*.info
164
+
165
+ # Visual Studio code coverage results
166
+ *.coverage
167
+ *.coveragexml
168
+
169
+ # NCrunch
170
+ _NCrunch_*
171
+ .NCrunch_*
172
+ .*crunch*.local.xml
173
+ nCrunchTemp_*
174
+
175
+ # MightyMoose
176
+ *.mm.*
177
+ AutoTest.Net/
178
+
179
+ # Web workbench (sass)
180
+ .sass-cache/
181
+
182
+ # Installshield output folder
183
+ [Ee]xpress/
184
+
185
+ # DocProject is a documentation generator add-in
186
+ DocProject/buildhelp/
187
+ DocProject/Help/*.HxT
188
+ DocProject/Help/*.HxC
189
+ DocProject/Help/*.hhc
190
+ DocProject/Help/*.hhk
191
+ DocProject/Help/*.hhp
192
+ DocProject/Help/Html2
193
+ DocProject/Help/html
194
+
195
+ # Click-Once directory
196
+ publish/
197
+
198
+ # Publish Web Output
199
+ *.[Pp]ublish.xml
200
+ *.azurePubxml
201
+ # Note: Comment the next line if you want to checkin your web deploy settings,
202
+ # but database connection strings (with potential passwords) will be unencrypted
203
+ *.pubxml
204
+ *.publishproj
205
+
206
+ # Microsoft Azure Web App publish settings. Comment the next line if you want to
207
+ # checkin your Azure Web App publish settings, but sensitive information contained
208
+ # in these scripts will be unencrypted
209
+ PublishScripts/
210
+
211
+ # NuGet Packages
212
+ *.nupkg
213
+ # NuGet Symbol Packages
214
+ *.snupkg
215
+ # The packages folder can be ignored because of Package Restore
216
+ **/[Pp]ackages/*
217
+ # except build/, which is used as an MSBuild target.
218
+ !**/[Pp]ackages/build/
219
+ # Uncomment if necessary however generally it will be regenerated when needed
220
+ #!**/[Pp]ackages/repositories.config
221
+ # NuGet v3's project.json files produces more ignorable files
222
+ *.nuget.props
223
+ *.nuget.targets
224
+
225
+ # Microsoft Azure Build Output
226
+ csx/
227
+ *.build.csdef
228
+
229
+ # Microsoft Azure Emulator
230
+ ecf/
231
+ rcf/
232
+
233
+ # Windows Store app package directories and files
234
+ AppPackages/
235
+ BundleArtifacts/
236
+ Package.StoreAssociation.xml
237
+ _pkginfo.txt
238
+ *.appx
239
+ *.appxbundle
240
+ *.appxupload
241
+
242
+ # Visual Studio cache files
243
+ # files ending in .cache can be ignored
244
+ *.[Cc]ache
245
+ # but keep track of directories ending in .cache
246
+ !?*.[Cc]ache/
247
+
248
+ # Others
249
+ ClientBin/
250
+ ~$*
251
+ *~
252
+ *.dbmdl
253
+ *.dbproj.schemaview
254
+ *.jfm
255
+ *.pfx
256
+ *.publishsettings
257
+ orleans.codegen.cs
258
+
259
+ # Including strong name files can present a security risk
260
+ # (https://github.com/github/gitignore/pull/2483#issue-259490424)
261
+ #*.snk
262
+
263
+ # Since there are multiple workflows, uncomment next line to ignore bower_components
264
+ # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
265
+ #bower_components/
266
+
267
+ # RIA/Silverlight projects
268
+ Generated_Code/
269
+
270
+ # Backup & report files from converting an old project file
271
+ # to a newer Visual Studio version. Backup files are not needed,
272
+ # because we have git ;-)
273
+ _UpgradeReport_Files/
274
+ Backup*/
275
+ UpgradeLog*.XML
276
+ UpgradeLog*.htm
277
+ ServiceFabricBackup/
278
+ *.rptproj.bak
279
+
280
+ # SQL Server files
281
+ *.mdf
282
+ *.ldf
283
+ *.ndf
284
+
285
+ # Business Intelligence projects
286
+ *.rdl.data
287
+ *.bim.layout
288
+ *.bim_*.settings
289
+ *.rptproj.rsuser
290
+ *- [Bb]ackup.rdl
291
+ *- [Bb]ackup ([0-9]).rdl
292
+ *- [Bb]ackup ([0-9][0-9]).rdl
293
+
294
+ # Microsoft Fakes
295
+ FakesAssemblies/
296
+
297
+ # GhostDoc plugin setting file
298
+ *.GhostDoc.xml
299
+
300
+ # Node.js Tools for Visual Studio
301
+ .ntvs_analysis.dat
302
+ node_modules/
303
+
304
+ # Visual Studio 6 build log
305
+ *.plg
306
+
307
+ # Visual Studio 6 workspace options file
308
+ *.opt
309
+
310
+ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
311
+ *.vbw
312
+
313
+ # Visual Studio 6 auto-generated project file (contains which files were open etc.)
314
+ *.vbp
315
+
316
+ # Visual Studio 6 workspace and project file (working project files containing files to include in project)
317
+ *.dsw
318
+ *.dsp
319
+
320
+ # Visual Studio 6 technical files
321
+ *.ncb
322
+ *.aps
323
+
324
+ # Visual Studio LightSwitch build output
325
+ **/*.HTMLClient/GeneratedArtifacts
326
+ **/*.DesktopClient/GeneratedArtifacts
327
+ **/*.DesktopClient/ModelManifest.xml
328
+ **/*.Server/GeneratedArtifacts
329
+ **/*.Server/ModelManifest.xml
330
+ _Pvt_Extensions
331
+
332
+ # Paket dependency manager
333
+ **/.paket/paket.exe
334
+ paket-files/
335
+
336
+ # FAKE - F# Make
337
+ **/.fake/
338
+
339
+ # CodeRush personal settings
340
+ **/.cr/personal
341
+
342
+ # Python Tools for Visual Studio (PTVS)
343
+ **/__pycache__/
344
+ *.pyc
345
+
346
+ # Cake - Uncomment if you are using it
347
+ #tools/**
348
+ #!tools/packages.config
349
+
350
+ # Tabs Studio
351
+ *.tss
352
+
353
+ # Telerik's JustMock configuration file
354
+ *.jmconfig
355
+
356
+ # BizTalk build output
357
+ *.btp.cs
358
+ *.btm.cs
359
+ *.odx.cs
360
+ *.xsd.cs
361
+
362
+ # OpenCover UI analysis results
363
+ OpenCover/
364
+
365
+ # Azure Stream Analytics local run output
366
+ ASALocalRun/
367
+
368
+ # MSBuild Binary and Structured Log
369
+ *.binlog
370
+ MSBuild_Logs/
371
+
372
+ # AWS SAM Build and Temporary Artifacts folder
373
+ .aws-sam
374
+
375
+ # NVidia Nsight GPU debugger configuration file
376
+ *.nvuser
377
+
378
+ # MFractors (Xamarin productivity tool) working folder
379
+ **/.mfractor/
380
+
381
+ # Local History for Visual Studio
382
+ **/.localhistory/
383
+
384
+ # Visual Studio History (VSHistory) files
385
+ .vshistory/
386
+
387
+ # BeatPulse healthcheck temp database
388
+ healthchecksdb
389
+
390
+ # Backup folder for Package Reference Convert tool in Visual Studio 2017
391
+ MigrationBackup/
392
+
393
+ # Ionide (cross platform F# VS Code tools) working folder
394
+ **/.ionide/
395
+
396
+ # Fody - auto-generated XML schema
397
+ FodyWeavers.xsd
398
+
399
+ # VS Code files for those working on multiple tools
400
+ .vscode/*
401
+ !.vscode/settings.json
402
+ !.vscode/tasks.json
403
+ !.vscode/launch.json
404
+ !.vscode/extensions.json
405
+ !.vscode/*.code-snippets
406
+
407
+ # Local History for Visual Studio Code
408
+ .history/
409
+
410
+ # Built Visual Studio Code Extensions
411
+ *.vsix
412
+
413
+ # Windows Installer files from build outputs
414
+ *.cab
415
+ *.msi
416
+ *.msix
417
+ *.msm
418
+ *.msp
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.5
2
+ Name: actornet
3
+ Version: 0.1.0
4
+ Summary: Python client for ActorNet, a hybrid .NET actor framework with virtual actors, supervision and clustering.
5
+ Project-URL: Homepage, https://github.com/DotNetVibeCoderz/Vibe_Library/tree/main/ActorNet
6
+ Project-URL: Documentation, https://github.com/DotNetVibeCoderz/Vibe_Library/blob/main/ActorNet/docs/en/08-clients.md
7
+ Project-URL: Repository, https://github.com/DotNetVibeCoderz/Vibe_Library
8
+ Project-URL: Issues, https://github.com/DotNetVibeCoderz/Vibe_Library/issues
9
+ Author: Kang Fadhil (Gravicode Studios)
10
+ License-Expression: MIT
11
+ Keywords: actor-model,actornet,distributed,rpc,virtual-actors
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: System :: Distributed Computing
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+
23
+ # actornet — Python client
24
+
25
+ Python client for [ActorNet](https://github.com/DotNetVibeCoderz/Vibe_Library/tree/main/ActorNet),
26
+ a hybrid .NET actor framework: Orleans-style virtual actors with Akka-style supervision,
27
+ clustering, persistence and event sourcing.
28
+
29
+ This package talks to an ActorNet node over the node's own wire protocol — a 4-byte big-endian
30
+ length followed by JSON. There is no HTTP gateway to deploy or keep in sync with the runtime.
31
+
32
+ No runtime dependencies: it is asyncio, `struct` and `json` from the standard library.
33
+
34
+ ```bash
35
+ pip install actornet
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```python
41
+ import asyncio
42
+ from actornet import ActorNetClient
43
+
44
+ async def main():
45
+ async with ActorNetClient(host="127.0.0.1", port=9000, client_id="ingest-1") as client:
46
+ # Fire and forget. Returns once the node accepts the message, not once it is handled.
47
+ await client.tell(
48
+ "BankAccountActor/alice",
49
+ "bank.deposit",
50
+ {"Amount": 500, "Reference": "opening"},
51
+ )
52
+
53
+ # Request/response.
54
+ reply = await client.ask("BankAccountActor/alice", "bank.get-statement", {"MaxEntries": 5})
55
+ print(reply.payload["Balance"])
56
+
57
+ asyncio.run(main())
58
+ ```
59
+
60
+ ## Addressing
61
+
62
+ Actors are addressed by string — `"BankAccountActor/alice"` is an actor type, a `/`, then a key.
63
+
64
+ Messages are addressed by **alias**, not by .NET type name. The node resolves an incoming alias
65
+ through an explicit allow-list, so an unregistered alias is refused rather than constructed. That
66
+ refusal is the security property — a transport that resolves whatever type name arrives lets a peer
67
+ choose what the process builds — and it is also what makes a Python process able to address the same
68
+ actors as a C# one.
69
+
70
+ On the .NET side an alias is declared on the message:
71
+
72
+ ```csharp
73
+ [ActorMessage(Alias = "bank.deposit")]
74
+ public sealed record Deposit(decimal Amount, string Reference = "");
75
+ ```
76
+
77
+ Payload keys are the .NET property names (`Amount`, `Reference`), matched case-insensitively.
78
+
79
+ ## What a client is, and is not
80
+
81
+ A client is **not** a cluster member. It connects to one node, and that node forwards to whichever
82
+ node owns the target actor — so any node is a valid entry point.
83
+
84
+ What it does not get is a membership view of its own. It cannot tell you where an actor lives, and
85
+ if the node it is connected to goes down it must reconnect elsewhere rather than failing over on its
86
+ own. Reconnect and cluster-aware routing are on the roadmap.
87
+
88
+ Because a client has no address the cluster can dial, the node answers on the connection the client
89
+ opened. That is why the client keeps one long-lived socket and keeps reading it even when only
90
+ sending: an `ask` reply has nowhere else to arrive.
91
+
92
+ ## Errors
93
+
94
+ | | |
95
+ | --- | --- |
96
+ | `AskTimeoutError` | No reply arrived in time. Usually the handler never calls `ReplyAsync`, or the actor's mailbox is long. |
97
+ | `ActorNetError` | The actor failed while handling the request, or the connection dropped. The node sends the failure back rather than leaving you on a timeout. |
98
+
99
+ ```python
100
+ from actornet import ActorNetClient, ActorNetError, AskTimeoutError
101
+
102
+ try:
103
+ reply = await client.ask("PaymentActor/cust-1", "order.charge", {"Amount": 50})
104
+ except AskTimeoutError:
105
+ ...
106
+ except ActorNetError as exc:
107
+ print(f"the actor failed: {exc}")
108
+ ```
109
+
110
+ ## Running the example
111
+
112
+ Start a node with the demo domain registered:
113
+
114
+ ```bash
115
+ dotnet run --project src/ActorNet.Cli -- run --port 9000
116
+ ```
117
+
118
+ Then:
119
+
120
+ ```bash
121
+ python examples/telemetry.py
122
+ ```
123
+
124
+ It streams 240 readings across 6 device actors — one actor per device, so each device's history
125
+ lands on a single activation with no locking — and reads back the aggregates and the alarm desk.
126
+
127
+ `ACTORNET_HOST` and `ACTORNET_PORT` override the defaults.
128
+
129
+ ## Documentation
130
+
131
+ - [Client SDKs and the wire protocol](https://github.com/DotNetVibeCoderz/Vibe_Library/blob/main/ActorNet/docs/en/08-clients.md)
132
+ - [ActorNet documentation](https://github.com/DotNetVibeCoderz/Vibe_Library/tree/main/ActorNet/docs/en)
133
+ - [Bahasa Indonesia](https://github.com/DotNetVibeCoderz/Vibe_Library/tree/main/ActorNet/docs/id)
134
+
135
+ ## License
136
+
137
+ MIT.
138
+
139
+ ---
140
+
141
+ Dibuat oleh **Gravicode Studios**, dipimpin oleh **Kang Fadhil**.
@@ -0,0 +1,119 @@
1
+ # actornet — Python client
2
+
3
+ Python client for [ActorNet](https://github.com/DotNetVibeCoderz/Vibe_Library/tree/main/ActorNet),
4
+ a hybrid .NET actor framework: Orleans-style virtual actors with Akka-style supervision,
5
+ clustering, persistence and event sourcing.
6
+
7
+ This package talks to an ActorNet node over the node's own wire protocol — a 4-byte big-endian
8
+ length followed by JSON. There is no HTTP gateway to deploy or keep in sync with the runtime.
9
+
10
+ No runtime dependencies: it is asyncio, `struct` and `json` from the standard library.
11
+
12
+ ```bash
13
+ pip install actornet
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```python
19
+ import asyncio
20
+ from actornet import ActorNetClient
21
+
22
+ async def main():
23
+ async with ActorNetClient(host="127.0.0.1", port=9000, client_id="ingest-1") as client:
24
+ # Fire and forget. Returns once the node accepts the message, not once it is handled.
25
+ await client.tell(
26
+ "BankAccountActor/alice",
27
+ "bank.deposit",
28
+ {"Amount": 500, "Reference": "opening"},
29
+ )
30
+
31
+ # Request/response.
32
+ reply = await client.ask("BankAccountActor/alice", "bank.get-statement", {"MaxEntries": 5})
33
+ print(reply.payload["Balance"])
34
+
35
+ asyncio.run(main())
36
+ ```
37
+
38
+ ## Addressing
39
+
40
+ Actors are addressed by string — `"BankAccountActor/alice"` is an actor type, a `/`, then a key.
41
+
42
+ Messages are addressed by **alias**, not by .NET type name. The node resolves an incoming alias
43
+ through an explicit allow-list, so an unregistered alias is refused rather than constructed. That
44
+ refusal is the security property — a transport that resolves whatever type name arrives lets a peer
45
+ choose what the process builds — and it is also what makes a Python process able to address the same
46
+ actors as a C# one.
47
+
48
+ On the .NET side an alias is declared on the message:
49
+
50
+ ```csharp
51
+ [ActorMessage(Alias = "bank.deposit")]
52
+ public sealed record Deposit(decimal Amount, string Reference = "");
53
+ ```
54
+
55
+ Payload keys are the .NET property names (`Amount`, `Reference`), matched case-insensitively.
56
+
57
+ ## What a client is, and is not
58
+
59
+ A client is **not** a cluster member. It connects to one node, and that node forwards to whichever
60
+ node owns the target actor — so any node is a valid entry point.
61
+
62
+ What it does not get is a membership view of its own. It cannot tell you where an actor lives, and
63
+ if the node it is connected to goes down it must reconnect elsewhere rather than failing over on its
64
+ own. Reconnect and cluster-aware routing are on the roadmap.
65
+
66
+ Because a client has no address the cluster can dial, the node answers on the connection the client
67
+ opened. That is why the client keeps one long-lived socket and keeps reading it even when only
68
+ sending: an `ask` reply has nowhere else to arrive.
69
+
70
+ ## Errors
71
+
72
+ | | |
73
+ | --- | --- |
74
+ | `AskTimeoutError` | No reply arrived in time. Usually the handler never calls `ReplyAsync`, or the actor's mailbox is long. |
75
+ | `ActorNetError` | The actor failed while handling the request, or the connection dropped. The node sends the failure back rather than leaving you on a timeout. |
76
+
77
+ ```python
78
+ from actornet import ActorNetClient, ActorNetError, AskTimeoutError
79
+
80
+ try:
81
+ reply = await client.ask("PaymentActor/cust-1", "order.charge", {"Amount": 50})
82
+ except AskTimeoutError:
83
+ ...
84
+ except ActorNetError as exc:
85
+ print(f"the actor failed: {exc}")
86
+ ```
87
+
88
+ ## Running the example
89
+
90
+ Start a node with the demo domain registered:
91
+
92
+ ```bash
93
+ dotnet run --project src/ActorNet.Cli -- run --port 9000
94
+ ```
95
+
96
+ Then:
97
+
98
+ ```bash
99
+ python examples/telemetry.py
100
+ ```
101
+
102
+ It streams 240 readings across 6 device actors — one actor per device, so each device's history
103
+ lands on a single activation with no locking — and reads back the aggregates and the alarm desk.
104
+
105
+ `ACTORNET_HOST` and `ACTORNET_PORT` override the defaults.
106
+
107
+ ## Documentation
108
+
109
+ - [Client SDKs and the wire protocol](https://github.com/DotNetVibeCoderz/Vibe_Library/blob/main/ActorNet/docs/en/08-clients.md)
110
+ - [ActorNet documentation](https://github.com/DotNetVibeCoderz/Vibe_Library/tree/main/ActorNet/docs/en)
111
+ - [Bahasa Indonesia](https://github.com/DotNetVibeCoderz/Vibe_Library/tree/main/ActorNet/docs/id)
112
+
113
+ ## License
114
+
115
+ MIT.
116
+
117
+ ---
118
+
119
+ Dibuat oleh **Gravicode Studios**, dipimpin oleh **Kang Fadhil**.
@@ -0,0 +1,9 @@
1
+ """ActorNet client for Python.
2
+
3
+ Dibuat oleh Gravicode Studios, dipimpin oleh Kang Fadhil.
4
+ """
5
+
6
+ from .client import ActorNetClient, ActorNetError, AskTimeoutError, Reply, WireKind
7
+
8
+ __all__ = ["ActorNetClient", "ActorNetError", "AskTimeoutError", "Reply", "WireKind"]
9
+ __version__ = "0.1.0"
@@ -0,0 +1,231 @@
1
+ """ActorNet client for Python.
2
+
3
+ Dibuat oleh Gravicode Studios, dipimpin oleh Kang Fadhil.
4
+
5
+ Speaks the node's own wire protocol - a 4-byte big-endian payload length followed by that many
6
+ bytes of JSON - so there is no separate gateway to keep in sync with the runtime.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ import struct
14
+ import uuid
15
+ from dataclasses import dataclass
16
+ from typing import Any, Dict, Optional
17
+
18
+ HEADER_BYTES = 4
19
+
20
+ # Refused above this, matching the node, so a bad length cannot make either side allocate wildly.
21
+ MAX_FRAME_BYTES = 32 * 1024 * 1024
22
+
23
+
24
+ class WireKind:
25
+ """Frame kinds. Must match ActorNet.Serialization.WireKind."""
26
+
27
+ MESSAGE = 1
28
+ ASK_REQUEST = 2
29
+ ASK_REPLY = 3
30
+ ASK_FAILURE = 4
31
+
32
+
33
+ class ActorNetError(Exception):
34
+ """Any failure reported by the node or by this client."""
35
+
36
+
37
+ class AskTimeoutError(ActorNetError):
38
+ """No reply arrived within the timeout."""
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Reply:
43
+ """An actor's answer: the alias it replied under, and the body."""
44
+
45
+ alias: str
46
+ payload: Any
47
+
48
+
49
+ class ActorNetClient:
50
+ """A connection to one ActorNet node.
51
+
52
+ One persistent socket, not one per message. An ask needs somewhere for the reply to arrive,
53
+ and the node addresses this client by the ``client_id`` stamped on every frame. Any node in a
54
+ cluster is a valid entry point: it forwards to whichever node owns the target actor.
55
+
56
+ Use as an async context manager::
57
+
58
+ async with ActorNetClient(port=9000) as client:
59
+ await client.tell("BankAccountActor/alice", "bank.deposit", {"Amount": 100})
60
+ reply = await client.ask("BankAccountActor/alice", "bank.get-statement", {})
61
+ print(reply.payload["Balance"])
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ host: str = "127.0.0.1",
67
+ port: int = 9000,
68
+ client_id: Optional[str] = None,
69
+ ask_timeout: float = 10.0,
70
+ ) -> None:
71
+ self.host = host
72
+ self.port = port
73
+ self.client_id = client_id or f"py-{uuid.uuid4().hex[:12]}"
74
+ self.ask_timeout = ask_timeout
75
+
76
+ self._reader: Optional[asyncio.StreamReader] = None
77
+ self._writer: Optional[asyncio.StreamWriter] = None
78
+ self._pending: Dict[str, asyncio.Future] = {}
79
+ self._read_task: Optional[asyncio.Task] = None
80
+ self._write_lock = asyncio.Lock()
81
+
82
+ async def __aenter__(self) -> "ActorNetClient":
83
+ await self.connect()
84
+ return self
85
+
86
+ async def __aexit__(self, *_exc_info: Any) -> None:
87
+ await self.close()
88
+
89
+ @property
90
+ def is_connected(self) -> bool:
91
+ return self._writer is not None and not self._writer.is_closing()
92
+
93
+ async def connect(self) -> None:
94
+ """Opens the connection. Called automatically by tell and ask."""
95
+ if self.is_connected:
96
+ return
97
+
98
+ self._reader, self._writer = await asyncio.open_connection(self.host, self.port)
99
+ self._read_task = asyncio.create_task(self._read_loop())
100
+
101
+ async def tell(self, target: str, alias: str, payload: Any) -> None:
102
+ """Fire-and-forget.
103
+
104
+ Returns once the frame is written, not once the actor has handled it.
105
+
106
+ Args:
107
+ target: Actor address, ``"Type/Key"``.
108
+ alias: Registered message alias, e.g. ``"bank.deposit"``.
109
+ payload: The message body.
110
+ """
111
+ await self.connect()
112
+ await self._write(
113
+ {
114
+ "k": WireKind.MESSAGE,
115
+ "t": target,
116
+ "a": alias,
117
+ "p": payload,
118
+ "f": self.client_id,
119
+ }
120
+ )
121
+
122
+ async def ask(
123
+ self,
124
+ target: str,
125
+ alias: str,
126
+ payload: Any,
127
+ timeout: Optional[float] = None,
128
+ ) -> Reply:
129
+ """Request/response.
130
+
131
+ Raises:
132
+ AskTimeoutError: no reply arrived in time.
133
+ ActorNetError: the actor failed while handling the request.
134
+ """
135
+ await self.connect()
136
+
137
+ correlation_id = uuid.uuid4().hex
138
+ window = self.ask_timeout if timeout is None else timeout
139
+
140
+ future: asyncio.Future = asyncio.get_running_loop().create_future()
141
+ self._pending[correlation_id] = future
142
+
143
+ try:
144
+ await self._write(
145
+ {
146
+ "k": WireKind.ASK_REQUEST,
147
+ "t": target,
148
+ "a": alias,
149
+ "p": payload,
150
+ "c": correlation_id,
151
+ # Both fields carry this client's id: "r" is what the actor's reply is routed
152
+ # by, and "f" is what the node keys this connection under.
153
+ "r": self.client_id,
154
+ "f": self.client_id,
155
+ }
156
+ )
157
+
158
+ try:
159
+ frame = await asyncio.wait_for(future, timeout=window)
160
+ except asyncio.TimeoutError as exc:
161
+ raise AskTimeoutError(f"No reply from '{target}' within {window:g}s.") from exc
162
+
163
+ if frame.get("k") == WireKind.ASK_FAILURE:
164
+ raise ActorNetError(frame.get("e") or f"Actor '{target}' failed while handling the request.")
165
+
166
+ return Reply(alias=frame.get("a", ""), payload=frame.get("p"))
167
+ finally:
168
+ self._pending.pop(correlation_id, None)
169
+
170
+ async def close(self) -> None:
171
+ """Closes the connection and fails anything still waiting."""
172
+ self._fail_pending(ActorNetError("The client was closed before a reply arrived."))
173
+
174
+ if self._read_task is not None:
175
+ self._read_task.cancel()
176
+ try:
177
+ await self._read_task
178
+ except (asyncio.CancelledError, Exception): # noqa: BLE001 - teardown must not raise
179
+ pass
180
+ self._read_task = None
181
+
182
+ if self._writer is not None:
183
+ self._writer.close()
184
+ try:
185
+ await self._writer.wait_closed()
186
+ except Exception: # noqa: BLE001 - the peer may already be gone
187
+ pass
188
+ self._writer = None
189
+
190
+ async def _write(self, frame: Dict[str, Any]) -> None:
191
+ body = json.dumps(frame, separators=(",", ":")).encode("utf-8")
192
+ if len(body) > MAX_FRAME_BYTES:
193
+ raise ActorNetError(f"Frame of {len(body)} bytes exceeds the {MAX_FRAME_BYTES} byte limit.")
194
+
195
+ # Several coroutines may be telling and asking at once; interleaved writes would produce
196
+ # frames neither of them sent.
197
+ async with self._write_lock:
198
+ assert self._writer is not None
199
+ self._writer.write(struct.pack(">i", len(body)) + body)
200
+ await self._writer.drain()
201
+
202
+ async def _read_loop(self) -> None:
203
+ try:
204
+ while True:
205
+ header = await self._reader.readexactly(HEADER_BYTES) # type: ignore[union-attr]
206
+ (length,) = struct.unpack(">i", header)
207
+
208
+ if length <= 0 or length > MAX_FRAME_BYTES:
209
+ raise ActorNetError(f"Node announced a frame length of {length} bytes.")
210
+
211
+ # readexactly is what makes this correct: TCP is a byte stream, so one reply can
212
+ # arrive in several chunks and two replies can arrive in one.
213
+ body = await self._reader.readexactly(length) # type: ignore[union-attr]
214
+ self._on_frame(json.loads(body.decode("utf-8")))
215
+ except asyncio.CancelledError:
216
+ raise
217
+ except asyncio.IncompleteReadError:
218
+ self._fail_pending(ActorNetError("The connection to the node closed before a reply arrived."))
219
+ except Exception as exc: # noqa: BLE001 - surfaced to every waiting caller
220
+ self._fail_pending(exc)
221
+
222
+ def _on_frame(self, frame: Dict[str, Any]) -> None:
223
+ future = self._pending.pop(frame.get("c", ""), None)
224
+ if future is not None and not future.done():
225
+ future.set_result(frame)
226
+
227
+ def _fail_pending(self, error: BaseException) -> None:
228
+ for correlation_id in list(self._pending):
229
+ future = self._pending.pop(correlation_id, None)
230
+ if future is not None and not future.done():
231
+ future.set_exception(error)
@@ -0,0 +1,92 @@
1
+ """Drives the ActorNet telemetry domain from Python.
2
+
3
+ Dibuat oleh Gravicode Studios, dipimpin oleh Kang Fadhil.
4
+
5
+ Start a node first::
6
+
7
+ dotnet run --project src/ActorNet.Cli -- run --port 9000
8
+
9
+ Then::
10
+
11
+ python clients/python/examples/telemetry.py
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import os
18
+ import random
19
+ import sys
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+
23
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
24
+
25
+ from actornet import ActorNetClient, ActorNetError # noqa: E402
26
+
27
+ HOST = os.environ.get("ACTORNET_HOST", "127.0.0.1")
28
+ PORT = int(os.environ.get("ACTORNET_PORT", "9000"))
29
+
30
+ DEVICES = 6
31
+ READINGS_PER_DEVICE = 40
32
+
33
+
34
+ async def main() -> int:
35
+ rng = random.Random(20260904)
36
+
37
+ async with ActorNetClient(host=HOST, port=PORT, client_id="python-example") as client:
38
+ print(f"connected to {HOST}:{PORT} as {client.client_id}")
39
+
40
+ # One actor per device. Routing by device id is what gives each device's readings a single
41
+ # writer without any locking on either side.
42
+ for reading in range(READINGS_PER_DEVICE):
43
+ for device in range(DEVICES):
44
+ # Device 2 runs hot, so the alarm path is exercised rather than just described.
45
+ baseline = 78.0 if device == 2 else 42.0
46
+ await client.tell(
47
+ f"DeviceActor/sensor-{device:03d}",
48
+ "iot.reading",
49
+ {
50
+ "DeviceId": f"sensor-{device:03d}",
51
+ "Celsius": baseline + rng.random() * 10,
52
+ "At": datetime.now(timezone.utc).isoformat(),
53
+ },
54
+ )
55
+
56
+ sent = DEVICES * READINGS_PER_DEVICE
57
+ print(f"streamed {sent} readings across {DEVICES} devices\n")
58
+
59
+ # Give the mailboxes a moment: a tell returns when the node accepts it, not when the
60
+ # actor has handled it.
61
+ await asyncio.sleep(0.3)
62
+
63
+ print(f"{'device':<14}{'latest':>9}{'average':>9}{'min':>8}{'max':>8}{'count':>8} alarm")
64
+ for device in range(DEVICES):
65
+ reply = await client.ask(f"DeviceActor/sensor-{device:03d}", "iot.get-status", {})
66
+ status = reply.payload
67
+ alarm = "YES" if status["InAlarm"] else "-"
68
+ print(
69
+ f"{status['DeviceId']:<14}"
70
+ f"{status['Latest']:>9.1f}"
71
+ f"{status['Average']:>9.1f}"
72
+ f"{status['Minimum']:>8.1f}"
73
+ f"{status['Maximum']:>8.1f}"
74
+ f"{status['Readings']:>8}"
75
+ f" {alarm}"
76
+ )
77
+
78
+ alarms = await client.ask("AlarmDeskActor/main", "iot.get-alarms", {})
79
+ active = alarms.payload["Devices"]
80
+ print(f"\nalarm desk: {len(active)} active, {alarms.payload['RaisedTotal']} raised in total")
81
+ for device in active:
82
+ print(f" {device}")
83
+
84
+ return 0
85
+
86
+
87
+ if __name__ == "__main__":
88
+ try:
89
+ raise SystemExit(asyncio.run(main()))
90
+ except ActorNetError as exc:
91
+ print(f"actornet: {exc}", file=sys.stderr)
92
+ raise SystemExit(1) from exc
@@ -0,0 +1,42 @@
1
+ # Dibuat oleh Gravicode Studios, dipimpin oleh Kang Fadhil.
2
+
3
+ [build-system]
4
+ requires = ["hatchling"]
5
+ build-backend = "hatchling.build"
6
+
7
+ [project]
8
+ name = "actornet"
9
+ version = "0.1.0"
10
+ description = "Python client for ActorNet, a hybrid .NET actor framework with virtual actors, supervision and clustering."
11
+ readme = "README.md"
12
+ requires-python = ">=3.10"
13
+ license = "MIT"
14
+ authors = [{ name = "Kang Fadhil (Gravicode Studios)" }]
15
+ keywords = ["actornet", "actor-model", "virtual-actors", "distributed", "rpc"]
16
+
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: System :: Distributed Computing",
25
+ "Framework :: AsyncIO",
26
+ ]
27
+
28
+ # No runtime dependencies on purpose. The whole client is asyncio, struct and json from the
29
+ # standard library, so adding it to a project cannot cause a version conflict.
30
+ dependencies = []
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/DotNetVibeCoderz/Vibe_Library/tree/main/ActorNet"
34
+ Documentation = "https://github.com/DotNetVibeCoderz/Vibe_Library/blob/main/ActorNet/docs/en/08-clients.md"
35
+ Repository = "https://github.com/DotNetVibeCoderz/Vibe_Library"
36
+ Issues = "https://github.com/DotNetVibeCoderz/Vibe_Library/issues"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["actornet"]
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ include = ["actornet", "examples", "README.md"]