httpstate 0.0.15__tar.gz → 0.0.19__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpstate
3
- Version: 0.0.15
3
+ Version: 0.0.19
4
4
  Summary: HTTPState, httpstate.com
5
5
  Author-email: "Alex Morales, HTTPState" <alex@httpstate.com>
6
6
  License-Expression: AGPL-3.0
@@ -9,7 +9,7 @@ Keywords: httpstate
9
9
  Classifier: Operating System :: OS Independent
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: websockets>=16.0
12
+ Requires-Dist: websockets>=15.0
13
13
  Description-Content-Type: text/markdown
14
14
 
15
15
  # httpstate (python)
@@ -8,7 +8,7 @@ classifiers = [
8
8
  "Operating System :: OS Independent",
9
9
  "Programming Language :: Python :: 3"
10
10
  ]
11
- dependencies = ["websockets>=16.0"]
11
+ dependencies = ["websockets>=15.0"]
12
12
  description = "HTTPState, httpstate.com"
13
13
  keywords = ["httpstate"]
14
14
  license = "AGPL-3.0"
@@ -16,7 +16,7 @@ license-files = ["LICEN[CS]E*"]
16
16
  name = "httpstate"
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.10"
19
- version = "0.0.15"
19
+ version = "0.0.19"
20
20
 
21
21
  [project.url]
22
22
  Homepage = "https://httpstate.com"
@@ -14,7 +14,7 @@ from .httpstate import(
14
14
  read,
15
15
  set,
16
16
  write,
17
- HttpState
17
+ HTTPState
18
18
  )
19
19
 
20
20
  __all__ = [
@@ -25,5 +25,5 @@ __all__ = [
25
25
  'read',
26
26
  'set',
27
27
  'write',
28
- 'HttpState'
28
+ 'HTTPState'
29
29
  ]
@@ -0,0 +1,263 @@
1
+ # HTTPState, https://httpstate.com/
2
+ # Copyright (C) Alex Morales, 2026
3
+
4
+ # Unless otherwise stated in particular files or directories, this software is free software.
5
+ # You can redistribute it and/or modify it under the terms of the GNU Affero
6
+ # General Public License as published by the Free Software Foundation, either
7
+ # version 3 of the License, or (at your option) any later version.
8
+
9
+ import asyncio
10
+ import datetime
11
+ import json
12
+ import struct
13
+ import threading
14
+ import urllib.error
15
+ import urllib.request
16
+ import websockets
17
+
18
+ from typing import Callable, Dict, List
19
+
20
+ def get(uuid:str, args:None|Dict = None) -> None|str|dict:
21
+ try:
22
+ req:urllib.request.Request = urllib.request.Request(f'https://httpstate.com/{uuid}')
23
+
24
+ if args and args.get('Authorization'):
25
+ req.add_header('Authorization', args.get('Authorization'))
26
+
27
+ with urllib.request.urlopen(req) as f:
28
+ if f.status == 200:
29
+ data:str = f.read().decode('utf-8', 'replace')
30
+
31
+ if(
32
+ not args
33
+ or (
34
+ not args.get('ETag')
35
+ and not args.get('Last-Modified')
36
+ )
37
+ ):
38
+ return data
39
+ else:
40
+ return {
41
+ **({ 'ETag':f.headers.get('ETag') } if args.get('ETag') else {}),
42
+ **({ 'Last-Modified':f.headers.get('Last-Modified') } if args.get('Last-Modified') else {}),
43
+ 'data':data
44
+ }
45
+ except urllib.error.HTTPError as e:
46
+ if e.code == 401:
47
+ raise Exception('401 Unauthorized')
48
+ elif e.code == 404:
49
+ raise Exception('404 Not Found')
50
+ elif e.code == 429:
51
+ raise Exception('429 Too Many Requests')
52
+ except Exception as e:
53
+ print(datetime.datetime.now().isoformat(), 'get.error', e)
54
+
55
+ raise e
56
+
57
+ class MessageType:
58
+ def __init__(self, uuid:str, timestamp:int, type:int, value:bytes) -> None:
59
+ self.uuid:str = uuid
60
+ self.timestamp:int = timestamp
61
+ self.type:int = type
62
+ self.value:bytes = value
63
+
64
+ class Message:
65
+ @staticmethod
66
+ def unpack(b:bytes) -> None|MessageType:
67
+ header:int = b[0]
68
+
69
+ if header == 0:
70
+ length:int = b[1]
71
+
72
+ return MessageType(
73
+ uuid=b[2:2+length].decode('utf-8'),
74
+ timestamp=struct.unpack_from('>Q', b, 2+length)[0],
75
+ type=b[2+length+8],
76
+ value=b[2+length+9:]
77
+ )
78
+
79
+ message:type = Message
80
+
81
+ def post(uuid:str, data:None|str = None, args:None|Dict = None) -> None|int:
82
+ return set(uuid, data, args)
83
+
84
+ def put(uuid:str, data:None|str = None, args:None|Dict = None) -> None|int:
85
+ return set(uuid, data, args)
86
+
87
+ def read(uuid:str, args:None|Dict = None) -> None|str|dict:
88
+ return get(uuid, args)
89
+
90
+ def set(uuid:str, data:None|str = None, args:None|Dict = None) -> None|int:
91
+ if(data is None):
92
+ data = ''
93
+
94
+ headers:Dict[str, str] = { 'Content-Type':'text/plain;charset=UTF-8' }
95
+
96
+ if args and args.get('Authorization'):
97
+ headers['Authorization'] = args.get('Authorization')
98
+
99
+ req:urllib.request.Request = urllib.request.Request(
100
+ f'https://httpstate.com/{uuid}',
101
+ data=data.encode('utf-8'),
102
+ headers=headers,
103
+ method='POST'
104
+ )
105
+
106
+ try:
107
+ with urllib.request.urlopen(req) as f:
108
+ return f.status
109
+ except urllib.error.HTTPError as e:
110
+ if e.code == 401:
111
+ raise Exception('401 Unauthorized')
112
+ elif e.code == 404:
113
+ raise Exception('404 Not Found')
114
+ elif e.code == 413:
115
+ raise Exception('413 Content Too Large')
116
+
117
+ return e.code
118
+ except Exception as e:
119
+ print(datetime.datetime.now().isoformat(), 'set.error', e)
120
+
121
+ def write(uuid:str, data:None|str = None, args:None|Dict = None) -> None|int:
122
+ return set(uuid, data, args)
123
+
124
+ # HTTPState
125
+ class HTTPState:
126
+ def __init__(self, uuid:str, args:None|Dict = None) -> None:
127
+ self.authorization:None|str = args.get('Authorization') if args else None
128
+ self.data:None|str = None
129
+ self.el:None|asyncio.AbstractEventLoop = None
130
+ self.et:None|Dict[str, List[Callable[[None|str], None]]] = {}
131
+ self.lock:None|threading.Lock = threading.Lock()
132
+ self.uuid:None|str = uuid
133
+ self.ws:None|websockets.WebSocketClientProtocol = None
134
+
135
+ threading.Thread(
136
+ daemon=True,
137
+ target=self._el
138
+ ).start()
139
+
140
+ threading.Thread(
141
+ daemon=True,
142
+ target=lambda : asyncio.run(self._ws())
143
+ ).start()
144
+
145
+ def _el(self) -> None:
146
+ self.el:asyncio.AbstractEventLoop = asyncio.new_event_loop()
147
+
148
+ asyncio.set_event_loop(self.el)
149
+
150
+ self.el.call_soon_threadsafe(lambda : self.get())
151
+
152
+ self.el.run_forever()
153
+
154
+ async def _ws(self) -> None:
155
+ self.ws:websockets.WebSocketClientProtocol = await websockets.connect(f"wss://httpstate.com/{self.uuid}")
156
+
157
+ await self.ws.send(json.dumps({ 'open':self.uuid, **({ 'Authorization':self.authorization } if self.authorization is not None else {}) }))
158
+
159
+ self.emit('open')
160
+
161
+ async def data() -> None:
162
+ async for data in self.ws:
163
+ data:None|MessageType = message.unpack(data)
164
+
165
+ if(
166
+ data
167
+ and data.uuid == self.uuid
168
+ and data.type == 1
169
+ ):
170
+ with self.lock:
171
+ self.data:None|str = data.value.decode()
172
+
173
+ self.emit('change', self.data)
174
+
175
+ asyncio.create_task(data())
176
+
177
+ async def interval() -> None:
178
+ while True:
179
+ try:
180
+ await self.ws.ping()
181
+
182
+ await asyncio.sleep(30) # 30 SECONDS
183
+ except websockets.ConnectionClosed:
184
+ break
185
+
186
+ asyncio.create_task(interval())
187
+
188
+ await asyncio.Event().wait()
189
+
190
+ def delete(self) -> None:
191
+ if self.el is not None:
192
+ if self.ws is not None:
193
+ asyncio.run_coroutine_threadsafe(self.ws.close(), self.el)
194
+
195
+ self.el.call_soon_threadsafe(self.el.stop)
196
+
197
+ self.authorization = None
198
+ self.data = None
199
+ self.el = None
200
+ self.et = None
201
+ self.lock = None
202
+ self.uuid = None
203
+ self.ws = None
204
+
205
+ def emit(self, type:str, data:None|str = None) -> "HTTPState":
206
+ if self.et is not None:
207
+ for callback in self.et.get(type, []):
208
+ if(data is None):
209
+ callback()
210
+ else:
211
+ callback(data)
212
+
213
+ return self
214
+
215
+ def get(self) -> None|str:
216
+ args:None|Dict = { 'Authorization':self.authorization } if self.authorization is not None else None
217
+ data:None|str = get(self.uuid, args)
218
+
219
+ with self.lock:
220
+ if(data != self.data):
221
+ if self.el is not None:
222
+ self.el.call_soon_threadsafe(lambda : self.emit('change', self.data))
223
+
224
+ self.data = data
225
+
226
+ return self.data
227
+
228
+ def off(self, type:str, callback:Callable[[None|str], None]) -> "HTTPState":
229
+ if self.et is not None and type in self.et:
230
+ try:
231
+ self.et[type].remove(callback)
232
+ except ValueError:
233
+ pass
234
+
235
+ if not self.et[type]:
236
+ del self.et[type]
237
+
238
+ return self
239
+
240
+ def on(self, type:str, callback:Callable[[None|str], None]) -> "HTTPState":
241
+ if self.et is not None:
242
+ if type not in self.et:
243
+ self.et[type] = []
244
+
245
+ self.et[type].append(callback)
246
+
247
+ return self
248
+
249
+ def post(self, data:None|str = None) -> None|int:
250
+ return self.set(data)
251
+
252
+ def put(self, data:None|str = None) -> None|int:
253
+ return self.set(data)
254
+
255
+ def read(self) -> None|str:
256
+ return self.get()
257
+
258
+ def set(self, data:None|str = None) -> None|int:
259
+ args:None|Dict = { 'Authorization':self.authorization } if self.authorization is not None else None
260
+ return set(self.uuid, data, args)
261
+
262
+ def write(self, data:None|str = None) -> None|int:
263
+ return self.set(data)
@@ -1,203 +0,0 @@
1
- # HTTPState, https://httpstate.com/
2
- # Copyright (C) Alex Morales, 2026
3
-
4
- # Unless otherwise stated in particular files or directories, this software is free software.
5
- # You can redistribute it and/or modify it under the terms of the GNU Affero
6
- # General Public License as published by the Free Software Foundation, either
7
- # version 3 of the License, or (at your option) any later version.
8
-
9
- import asyncio
10
- import struct
11
- import threading
12
- import urllib.error
13
- import urllib.request
14
- import websockets
15
-
16
- from typing import Callable, Dict, List
17
-
18
- def get(uuid:str) -> None|str:
19
- try:
20
- with urllib.request.urlopen(f'https://httpstate.com/{uuid}') as f:
21
- if f.status == 200:
22
- return f.read().decode('utf-8', 'replace')
23
-
24
- return None
25
- except urllib.error.HTTPError:
26
- return None
27
- except Exception:
28
- return None
29
-
30
- class MessageType:
31
- def __init__(self, uuid:str, timestamp:int, type:int, value:bytes) -> None:
32
- self.uuid:str = uuid
33
- self.timestamp:int = timestamp
34
- self.type:int = type
35
- self.value:bytes = value
36
-
37
- class Message:
38
- @staticmethod
39
- def unpack(b:bytes) -> MessageType:
40
- length:int = b[0]
41
-
42
- return MessageType(
43
- uuid=b[1:1+length].decode('utf-8'),
44
- timestamp=struct.unpack_from('>Q', b, 1+length)[0],
45
- type=b[1+length+8],
46
- value=b[1+length+9:],
47
- )
48
-
49
- message:type = Message
50
-
51
- def post(uuid:str, data:None|str = None) -> None|int:
52
- return set(uuid, data)
53
-
54
- def put(uuid:str, data:None|str = None) -> None|int:
55
- return set(uuid, data)
56
-
57
- def read(uuid:str) -> None|str:
58
- return get(uuid)
59
-
60
- def set(uuid:str, data:None|str = None) -> None|int:
61
- if(data is None):
62
- data = ''
63
-
64
- req:urllib.request.Request = urllib.request.Request(
65
- f'https://httpstate.com/{uuid}',
66
- data=data.encode('utf-8'),
67
- headers={ 'Content-Type':'text/plain;charset=UTF-8' },
68
- method='POST'
69
- )
70
-
71
- try:
72
- with urllib.request.urlopen(req) as f:
73
- return f.status
74
- except urllib.error.HTTPError as e:
75
- return e.code
76
- except Exception:
77
- return None
78
-
79
- def write(uuid:str, data:None|str = None) -> None|int:
80
- return set(uuid, data)
81
-
82
- # HTTPState
83
- class HttpState:
84
- def __init__(self, uuid:str) -> None:
85
- self.data:None|str = None
86
- self.el:None|asyncio.AbstractEventLoop = None
87
- self.et:Dict[str, List[Callable[[None|str], None]]] = {}
88
- self.lock:threading.Lock = threading.Lock()
89
- self.uuid:str = uuid
90
- self.ws:None|websockets.WebSocketClientProtocol = None
91
-
92
- threading.Thread(
93
- daemon=True,
94
- target=self._el
95
- ).start()
96
-
97
- threading.Thread(
98
- daemon=True,
99
- target=lambda : asyncio.run(self._ws())
100
- ).start()
101
-
102
- def _el(self) -> None:
103
- self.el:asyncio.AbstractEventLoop = asyncio.new_event_loop()
104
-
105
- asyncio.set_event_loop(self.el)
106
-
107
- self.el.call_soon_threadsafe(lambda : self.get())
108
-
109
- self.el.run_forever()
110
-
111
- async def _ws(self) -> None:
112
- self.ws:websockets.WebSocketClientProtocol = await websockets.connect(f"wss://httpstate.com/{self.uuid}")
113
-
114
- await self.ws.send(f'{{"open":"{self.uuid}"}}')
115
- self.emit('open')
116
-
117
- async def data() -> None:
118
- async for data in self.ws:
119
- data:MessageType = message.unpack(data)
120
-
121
- if(
122
- data
123
- and data.uuid == self.uuid
124
- and data.type == 1
125
- ):
126
- with self.lock:
127
- self.data:None|str = data.value.decode()
128
-
129
- self.emit('change', self.data)
130
-
131
- asyncio.create_task(data())
132
-
133
- async def interval() -> None:
134
- while True:
135
- try:
136
- await self.ws.ping()
137
-
138
- await asyncio.sleep(30) # 30 SECONDS
139
- except websockets.ConnectionClosed:
140
- break
141
-
142
- asyncio.create_task(interval())
143
-
144
- await asyncio.Event().wait()
145
-
146
- def delete(self) -> None:
147
- pass
148
-
149
- def emit(self, type:str, data:None|str = None) -> "HttpState":
150
- for callback in self.et.get(type, []):
151
- if(data is None):
152
- callback()
153
- else:
154
- callback(data)
155
-
156
- return self
157
-
158
- def get(self) -> None|str:
159
- data:None|str = get(self.uuid)
160
-
161
- with self.lock:
162
- if(data != self.data):
163
- if self.el is not None:
164
- self.el.call_soon_threadsafe(lambda : self.emit('change', self.data))
165
-
166
- self.data = data
167
-
168
- return self.data
169
-
170
- def off(self, type:str, callback:Callable[[None|str], None]) -> "HttpState":
171
- if type in self.et:
172
- try:
173
- self.et[type].remove(callback)
174
- except ValueError:
175
- pass
176
-
177
- if not self.et[type]:
178
- del self.et[type]
179
-
180
- return self
181
-
182
- def on(self, type:str, callback:Callable[[None|str], None]) -> "HttpState":
183
- if type not in self.et:
184
- self.et[type] = []
185
-
186
- self.et[type].append(callback)
187
-
188
- return self
189
-
190
- def post(self, data:None|str = None) -> None|int:
191
- return self.set(data)
192
-
193
- def put(self, data:None|str = None) -> None|int:
194
- return self.set(data)
195
-
196
- def read(self) -> None|str:
197
- return self.get()
198
-
199
- def set(self, data:None|str = None) -> None|int:
200
- return set(self.uuid, data)
201
-
202
- def write(self, data:None|str = None) -> None|int:
203
- return self.set(data)
File without changes
File without changes
File without changes