streamrelay 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,28 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python ${{ matrix.python-version }}
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+
24
+ - name: Install dependencies
25
+ run: pip install -e ".[dev]"
26
+
27
+ - name: Run tests
28
+ run: pytest tests/ -v
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .pytest_cache/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .env
9
+ *.egg
@@ -0,0 +1,228 @@
1
+ # Contributing & Testing Guide
2
+
3
+ Three levels of testing are available, each building on the previous:
4
+
5
+ 1. **Unit + integration tests** — no network, no credentials, runs in < 2s
6
+ 2. **Local end-to-end** — full producer → relay → consumer on your laptop via Cloudflare tunnel
7
+ 3. **Live relay** — test against a real public relay server with authentication and encryption
8
+
9
+ ---
10
+
11
+ ## Level 1 — Unit and integration tests (no network needed)
12
+
13
+ ```bash
14
+ git clone https://github.com/uicacer/streamrelay
15
+ cd streamrelay
16
+ pip install -e ".[dev]"
17
+ pytest tests/ -v
18
+ ```
19
+
20
+ Expected output:
21
+ ```
22
+ tests/test_crypto.py::test_round_trip PASSED
23
+ tests/test_crypto.py::test_encrypted_format PASSED
24
+ tests/test_crypto.py::test_fresh_nonce_each_call PASSED
25
+ tests/test_crypto.py::test_passthrough_non_enc PASSED
26
+ tests/test_crypto.py::test_wrong_key_raises PASSED
27
+ tests/test_crypto.py::test_generate_key_length PASSED
28
+ tests/test_integration.py::test_basic PASSED
29
+ tests/test_integration.py::test_buffering_producer_first PASSED
30
+ tests/test_integration.py::test_encrypted PASSED
31
+ tests/test_integration.py::test_empty_stream PASSED
32
+ 10 passed in ~1s
33
+ ```
34
+
35
+ The integration tests spin up a real relay on `localhost:18765` and run
36
+ producer/consumer pairs through it, including buffering and AES-256-GCM encryption.
37
+ No HPC, no public server, no credentials required.
38
+
39
+ ---
40
+
41
+ ## Level 2 — Local end-to-end with Cloudflare Tunnel
42
+
43
+ Tests the complete flow across a real network, simulating HPC and client on
44
+ separate machines — entirely on your laptop.
45
+
46
+ **Prerequisites:**
47
+ ```bash
48
+ pip install streamrelay
49
+ brew install cloudflared # macOS
50
+ # Linux: https://pkg.cloudflare.com/index.html
51
+ ```
52
+
53
+ **Terminal 1 — Start the relay:**
54
+ ```bash
55
+ streamrelay --port 8765 --secret demo-secret
56
+ # Output: streamrelay listening on ws://0.0.0.0:8765
57
+ ```
58
+
59
+ **Terminal 2 — Expose it publicly:**
60
+ ```bash
61
+ cloudflared tunnel --url http://localhost:8765
62
+ # Output: ... Your quick Tunnel has been created! Visit it at (it's https, not http):
63
+ # https://random-name.trycloudflare.com
64
+ ```
65
+ Copy that URL and replace `https://` with `wss://` → your `RELAY_URL`.
66
+
67
+ **Terminal 3 — Consumer (simulates your application):**
68
+ ```python
69
+ # consumer_test.py
70
+ from streamrelay import RelayConsumer
71
+
72
+ RELAY_URL = "wss://random-name.trycloudflare.com" # from Terminal 2
73
+ CHANNEL = "test-channel-001"
74
+ SECRET = "demo-secret"
75
+
76
+ print("Waiting for tokens...")
77
+ for token in RelayConsumer(RELAY_URL, CHANNEL, relay_secret=SECRET).stream():
78
+ print(token, end="", flush=True)
79
+ print() # newline after last token
80
+ ```
81
+ ```bash
82
+ python consumer_test.py
83
+ ```
84
+
85
+ **Terminal 4 — Producer (simulates the HPC compute node):**
86
+ ```python
87
+ # producer_test.py
88
+ from streamrelay import RelayProducer
89
+
90
+ RELAY_URL = "wss://random-name.trycloudflare.com" # same URL
91
+ CHANNEL = "test-channel-001"
92
+ SECRET = "demo-secret"
93
+
94
+ with RelayProducer(RELAY_URL, CHANNEL, relay_secret=SECRET) as relay:
95
+ for word in ["Hello", " ", "from", " ", "the", " ", "compute", " ", "node", "!"]:
96
+ relay.send_token(word)
97
+ ```
98
+ ```bash
99
+ python producer_test.py
100
+ ```
101
+
102
+ You should see `Hello from the compute node!` appear token-by-token in Terminal 3
103
+ as Terminal 4 sends each word.
104
+
105
+ **What this proves:** both sides connect outbound to a public relay across the real
106
+ internet, with shared-secret authentication, without any inbound ports open.
107
+
108
+ ---
109
+
110
+ ## Level 3 — Live relay test (authentication + encryption)
111
+
112
+ Run the following script against a real relay server to verify all three security
113
+ layers work end-to-end. Replace `RELAY_URL` and `SECRET` with your relay's values.
114
+
115
+ ```python
116
+ # live_relay_test.py
117
+ import asyncio
118
+ import uuid
119
+ from streamrelay import RelayProducer, RelayConsumer, generate_key
120
+
121
+
122
+ RELAY_URL = "wss://your-relay.example.com"
123
+ SECRET = "your-relay-secret"
124
+
125
+
126
+ async def test_plain():
127
+ """Plain streaming — no encryption."""
128
+ channel_id = str(uuid.uuid4())
129
+ received = []
130
+
131
+ async def consume():
132
+ async for token in RelayConsumer(RELAY_URL, channel_id, relay_secret=SECRET):
133
+ received.append(token)
134
+
135
+ async def produce():
136
+ await asyncio.sleep(0.3)
137
+ async with RelayProducer(RELAY_URL, channel_id, relay_secret=SECRET) as p:
138
+ for word in ["Hello", " ", "from", " ", "live", " ", "relay", "!"]:
139
+ await p._async_send_raw({"type": "token", "content": word})
140
+
141
+ await asyncio.gather(consume(), produce())
142
+ result = "".join(received)
143
+ assert result == "Hello from live relay!", f"Got: {repr(result)}"
144
+ print(f"[PASS] plain streaming: {repr(result)}")
145
+
146
+
147
+ async def test_encrypted():
148
+ """AES-256-GCM end-to-end encryption — relay sees only ciphertext."""
149
+ channel_id = str(uuid.uuid4())
150
+ key = generate_key()
151
+ received = []
152
+
153
+ async def consume():
154
+ async for token in RelayConsumer(RELAY_URL, channel_id,
155
+ relay_secret=SECRET, encryption_key=key):
156
+ received.append(token)
157
+
158
+ async def produce():
159
+ await asyncio.sleep(0.3)
160
+ async with RelayProducer(RELAY_URL, channel_id,
161
+ relay_secret=SECRET, encryption_key=key) as p:
162
+ for word in ["encrypted", " ", "live", " ", "test"]:
163
+ await p._async_send_raw({"type": "token", "content": word})
164
+
165
+ await asyncio.gather(consume(), produce())
166
+ result = "".join(received)
167
+ assert result == "encrypted live test", f"Got: {repr(result)}"
168
+ print(f"[PASS] encrypted streaming: {repr(result)}")
169
+
170
+
171
+ async def test_wrong_secret_rejected():
172
+ """Wrong secret must be rejected by the relay with code 4003."""
173
+ channel_id = str(uuid.uuid4())
174
+ try:
175
+ async for _ in RelayConsumer(RELAY_URL, channel_id, relay_secret="wrong"):
176
+ pass
177
+ print("[FAIL] wrong secret was not rejected")
178
+ except Exception as e:
179
+ assert "4003" in str(e) or "Forbidden" in str(e), f"Unexpected error: {e}"
180
+ print(f"[PASS] wrong secret rejected: {type(e).__name__}")
181
+
182
+
183
+ async def main():
184
+ print(f"Testing relay: {RELAY_URL}\n")
185
+ await test_plain()
186
+ await test_encrypted()
187
+ await test_wrong_secret_rejected()
188
+ print("\nAll live relay tests passed.")
189
+
190
+
191
+ asyncio.run(main())
192
+ ```
193
+
194
+ ```bash
195
+ python live_relay_test.py
196
+ ```
197
+
198
+ Expected output:
199
+ ```
200
+ Testing relay: wss://your-relay.example.com
201
+
202
+ [PASS] plain streaming: 'Hello from live relay!'
203
+ [PASS] encrypted streaming: 'encrypted live test'
204
+ [PASS] wrong secret rejected: ConnectionClosedError
205
+
206
+ All live relay tests passed.
207
+ ```
208
+
209
+ ---
210
+
211
+ ## Development setup
212
+
213
+ ```bash
214
+ git clone https://github.com/uicacer/streamrelay
215
+ cd streamrelay
216
+ pip install -e ".[dev,globus]"
217
+ pytest tests/ -v
218
+ ruff check streamrelay/
219
+ ```
220
+
221
+ ---
222
+
223
+ ## Submitting changes
224
+
225
+ 1. Fork the repo and create a feature branch.
226
+ 2. `pytest tests/ -v` — all 10 tests must pass.
227
+ 3. `ruff check streamrelay/` — no lint errors.
228
+ 4. Open a pull request with a description of what and why.
@@ -0,0 +1,153 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship made available under
36
+ the License, as indicated by a copyright notice that is included in
37
+ or attached to the work (an example is provided in the Appendix below).
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship. For the purposes
43
+ of this License, Derivative Works shall not include works that remain
44
+ separable from, or merely link (or bind by name) to the interfaces of,
45
+ the Work and Derivative Works thereof.
46
+
47
+ "Contribution" shall mean, as defined by Section 1(a), any work of
48
+ authorship submitted to the Licensor for inclusion in the Work by the
49
+ copyright owner or by an individual or Legal Entity authorized to
50
+ submit on behalf of the copyright owner.
51
+
52
+ "Contributor" shall mean Licensor and any Legal Entity on behalf of
53
+ whom a Contribution has been received by the Licensor and included
54
+ within the Work.
55
+
56
+ 2. Grant of Copyright License. Subject to the terms and conditions of
57
+ this License, each Contributor hereby grants to You a perpetual,
58
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
59
+ copyright license to reproduce, prepare Derivative Works of,
60
+ publicly display, publicly perform, sublicense, and distribute the
61
+ Work and such Derivative Works in Source or Object form.
62
+
63
+ 3. Grant of Patent License. Subject to the terms and conditions of
64
+ this License, each Contributor hereby grants to You a perpetual,
65
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
66
+ (except as stated in this section) patent license to make, have made,
67
+ use, offer to sell, sell, import, and otherwise transfer the Work,
68
+ where such license applies only to those patent claims licensable
69
+ by such Contributor that are necessarily infringed by their
70
+ Contribution(s) alone or by the combinations of their Contribution(s)
71
+ with the Work to which such Contribution(s) was submitted. If You
72
+ institute patent proceedings against any entity (including a
73
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
74
+ or a Contribution incorporated within the Work constitutes direct
75
+ or contributory patent infringement, then any patent licenses
76
+ granted to You under this License for that Work shall terminate
77
+ as of the date such litigation is filed.
78
+
79
+ 4. Redistribution. You may reproduce and distribute copies of the
80
+ Work or Derivative Works thereof in any medium, with or without
81
+ modifications, and in Source or Object form, provided that You
82
+ meet the following conditions:
83
+
84
+ (a) You must give any other recipients of the Work or Derivative
85
+ Works a copy of this License; and
86
+
87
+ (b) You must cause any modified files to carry prominent notices
88
+ stating that You changed the files; and
89
+
90
+ (c) You must retain, in the Source form of any Derivative Works
91
+ that You distribute, all copyright, patent, trademark, and
92
+ attribution notices from the Source form of the Work,
93
+ excluding those notices that do not pertain to any part of
94
+ the Derivative Works; and
95
+
96
+ (d) If the Work includes a "NOTICE" text file as part of its
97
+ distribution, You must include a readable copy of the
98
+ attribution notices contained within such NOTICE file, in
99
+ at least one of the following places: within a NOTICE text
100
+ file distributed as part of the Derivative Works; within
101
+ the Source form or documentation, if provided along with the
102
+ Derivative Works; or, within a display generated by the
103
+ Derivative Works, if and wherever such third-party notices
104
+ normally appear. The contents of the NOTICE file are for
105
+ informational purposes only and do not modify the License.
106
+
107
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
108
+ any Contribution intentionally submitted for inclusion in the Work
109
+ by You to the Licensor shall be under the terms and conditions of
110
+ this License, without any additional terms or conditions.
111
+
112
+ 6. Trademarks. This License does not grant permission to use the trade
113
+ names, trademarks, service marks, or product names of the Licensor,
114
+ except as required for reasonable and customary use in describing the
115
+ origin of the Work and reproducing the content of the NOTICE file.
116
+
117
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed
118
+ to in writing, Licensor provides the Work (and each Contributor
119
+ provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES
120
+ OR CONDITIONS OF ANY KIND, either express or implied, including,
121
+ without limitation, any warranties or conditions of TITLE,
122
+ NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
123
+
124
+ 8. Limitation of Liability. In no event and under no legal theory,
125
+ whether in tort (including negligence), contract, or otherwise,
126
+ unless required by applicable law (such as deliberate and grossly
127
+ negligent acts) or agreed to in writing, shall any Contributor be
128
+ liable to You for damages, including any direct, indirect, special,
129
+ incidental, or exemplary damages of any kind arising as a result of
130
+ this License or out of the use or inability to use the Work.
131
+
132
+ 9. Accepting Warranty or Liability. While redistributing the Work or
133
+ Derivative Works thereof, You may choose to offer, and charge a fee
134
+ for, acceptance of support, warranty, indemnity, or other liability
135
+ obligations and/or rights consistent to the Law. However, in accepting
136
+ such obligations, You may offer such obligations only on Your own behalf
137
+ and on Your sole responsibility, not on behalf of any other Contributor.
138
+
139
+ END OF TERMS AND CONDITIONS
140
+
141
+ Copyright 2026 Anas Nassar, University of Illinois Chicago
142
+
143
+ Licensed under the Apache License, Version 2.0 (the "License");
144
+ you may not use this file except in compliance with the License.
145
+ You may obtain a copy of the License at
146
+
147
+ http://www.apache.org/licenses/LICENSE-2.0
148
+
149
+ Unless required by applicable law or agreed to in writing, software
150
+ distributed under the License is distributed on an "AS IS" BASIS,
151
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
152
+ See the License for the specific language governing permissions and
153
+ limitations under the License.