Skip to content

Commit 374eac4

Browse files
committed
Fix major issues
1 parent 2ebd8b3 commit 374eac4

8 files changed

Lines changed: 110 additions & 89 deletions

File tree

pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[project]
2+
name = "talkops"
3+
version = "1.0.0"
4+
description = "TalkOps SDK"
5+
authors = [{ name = "PicoUX" }]
6+
license = "MIT"
7+
requires-python = ">=3.7"
8+
dependencies = [
9+
"jinja2>=3.1.6",
10+
"requests>=2.32.3",
11+
"sseclient-py>=1.8.0"
12+
]

setup.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/talkops/extension.py

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
1-
import os
2-
import json
3-
import base64
41
from urllib.parse import urlparse
52
from .publisher import Publisher
63
from .subscriber import Subscriber
74
from .parameter import Parameter
85
from .readme import Readme
96
from .manifest import Manifest
7+
import asyncio
8+
import json
9+
import base64
10+
import os
1011
import pkg_resources
1112

1213
class Extension:
1314
def __init__(self, token=None):
14-
token = token or os.environ.get('TALKOPS_TOKEN')
15-
if token:
16-
mercure = json.loads(base64.b64decode(token).decode())
15+
self._callbacks = {}
16+
self._category = None
17+
self._demo = False
18+
self._features = []
19+
self._functions = []
20+
self._function_schemas = []
21+
self._icon = None
22+
self._installation_steps = []
23+
self._instructions = None
24+
self._name = None
25+
self._parameters = []
26+
self._software_version = None
27+
self._token = token or os.environ.get('TALKOPS_TOKEN')
28+
self._website = None
29+
30+
async def _setup(self):
31+
await asyncio.sleep(0.5)
32+
if self._token:
33+
mercure = json.loads(base64.b64decode(self._token).decode())
1734
self._publisher = Publisher(
1835
lambda: {'mercure': mercure},
1936
lambda: {
@@ -43,7 +60,7 @@ def __init__(self, token=None):
4360
}
4461
)
4562

46-
if os.environ.get('NODE_ENV') == 'development':
63+
if os.environ.get('ENV') == 'development':
4764
Readme(
4865
lambda: {
4966
'features': self._features,
@@ -59,26 +76,16 @@ def __init__(self, token=None):
5976
'name': self._name,
6077
'sdk': {
6178
'name': 'python',
62-
'version': '2.14.1',
79+
'version': pkg_resources.get_distribution('talkops').version,
6380
},
6481
'softwareVersion': self._software_version,
6582
'website': self._website,
6683
}
6784
)
6885

69-
self._callbacks = {}
70-
self._category = None
71-
self._demo = False
72-
self._features = []
73-
self._functions = []
74-
self._function_schemas = []
75-
self._icon = None
76-
self._installation_steps = []
77-
self._instructions = None
78-
self._name = None
79-
self._parameters = []
80-
self._software_version = None
81-
self._website = None
86+
def start(self):
87+
asyncio.run(self._setup())
88+
return self
8289

8390
def on(self, event_type, callback):
8491
if event_type not in self._get_event_types():
@@ -155,37 +162,55 @@ def set_instructions(self, instructions):
155162
return self
156163

157164
def set_function_schemas(self, function_schemas):
158-
if not isinstance(function_schemas, list):
159-
raise ValueError('function_schemas must be an array.')
165+
if (
166+
not isinstance(function_schemas, list) or
167+
not all(isinstance(schema, dict) and schema is not None for schema in function_schemas)
168+
):
169+
raise ValueError('functionSchemas must be an array of non-null objects.')
160170
self._function_schemas = function_schemas
161171
return self
162172

163173
def set_functions(self, functions):
164-
if not isinstance(functions, list):
165-
raise ValueError('functions must be an array.')
174+
if (
175+
not isinstance(functions, list) or
176+
not all(callable(fn) for fn in functions) or
177+
not all(hasattr(fn, '__name__') and fn.__name__.strip() for fn in functions)
178+
):
179+
raise ValueError('functions must be an array of named functions.')
166180
self._functions = functions
167181
return self
168182

169183
def enable_alarm(self):
170-
self._publisher.enable_alarm()
184+
self._publisher.publish_event({'type': 'alarm'})
171185
return self
172186

173187
def send_medias(self, medias):
174-
self._publisher.send_medias(medias)
188+
if not isinstance(medias, list):
189+
medias = [medias]
190+
if not all(isinstance(media, Media) for media in medias):
191+
raise ValueError("medias must be a list of Media instances.")
192+
self._publisher.publish_event({
193+
'type': 'medias',
194+
'medias': [media.to_json() for media in medias]
195+
})
175196
return self
176197

177198
def send_message(self, text):
178-
self._publisher.send_message(text)
199+
if not isinstance(text, str) or not text.strip():
200+
raise ValueError('text must be a non-empty string.')
201+
self._publisher.publish_event({ 'type': 'message', 'text': text })
179202
return self
180203

181204
def send_notification(self, text):
182-
self._publisher.send_notification(text)
205+
if not isinstance(text, str) or not text.strip():
206+
raise ValueError('text must be a non-empty string.')
207+
self._publisher.publish_event({ 'type': 'notification', 'text': text })
183208
return self
184209

185-
def _get_event_types(self):
186-
with open(os.path.join(os.path.dirname(__file__), 'event_types.json'), 'r') as f:
187-
return json.load(f)
188-
189210
def _get_categories(self):
190211
with open(os.path.join(os.path.dirname(__file__), 'categories.json'), 'r') as f:
191212
return json.load(f)
213+
214+
def _get_event_types(self):
215+
with open(os.path.join(os.path.dirname(__file__), 'event-types.json'), 'r') as f:
216+
return json.load(f)

src/talkops/manifest.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
import json
33

44
class Manifest:
5-
def __init__(self, get_config):
6-
self._get_config = get_config
7-
time.sleep(0.5)
5+
def __init__(self, use_extension):
6+
self._use_extension = use_extension
87
self._generate()
98

109
def _generate(self):
1110
with open('/app/manifest.json', 'w') as f:
12-
json.dump(self._get_config(), f, indent=2)
11+
json.dump(self._use_extension(), f, indent=2)

src/talkops/publisher.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ def __init__(self, use_config, use_state):
1111
self._use_state = use_state
1212
self._last_event_state = None
1313
self._last_ping_at = None
14-
threading.Timer(0.9, lambda: self._publish_data(json.dumps({'type': 'init'}))).start()
15-
threading.Timer(1.0, self._publish_state).start()
14+
threading.Timer(0.4, lambda: self._publish_data(json.dumps({'type': 'init'}))).start()
15+
threading.Timer(0.5, self._publish_state).start()
1616
self._original_stdout_write = sys.stdout.write
1717
self._original_stderr_write = sys.stderr.write
1818
def stdout_wrapper(chunk):
@@ -23,30 +23,31 @@ def stdout_wrapper(chunk):
2323
return self._original_stdout_write(chunk)
2424

2525
def stderr_wrapper(chunk):
26-
self.publish_event({
27-
'type': 'stderr',
28-
'data': chunk.strip()
29-
})
26+
if b"KeyboardInterrupt" not in chunk:
27+
self.publish_event({
28+
'type': 'stderr',
29+
'data': chunk.strip()
30+
})
3031
return self._original_stderr_write(chunk)
3132

3233
sys.stdout.write = stdout_wrapper
3334
sys.stderr.write = stderr_wrapper
3435

35-
async def publish_state(self):
36+
def publish_state(self):
3637
event = {'type': 'state', 'state': self._use_state()}
3738
self._last_event_state = json.dumps(event)
38-
await self.publish_event(event)
39+
self.publish_event(event)
3940

4041
def on_ping(self):
4142
self._last_ping_at = time.time() * 1000
4243
self.publish_event({'type': 'pong'})
4344

44-
async def publish_event(self, event):
45+
def publish_event(self, event):
4546
if self._last_ping_at and self._last_ping_at < (time.time() * 1000 - 6000):
4647
return
47-
await self._publish_data(json.dumps(event))
48+
self._publish_data(json.dumps(event))
4849

49-
async def _publish_data(self, data):
50+
def _publish_data(self, data):
5051
config = self._use_config()
5152
response = requests.post(
5253
config['mercure']['url'],
@@ -61,11 +62,11 @@ async def _publish_data(self, data):
6162
)
6263
response.raise_for_status()
6364

64-
async def _publish_state(self):
65+
def _publish_state(self):
6566
event = {'type': 'state', 'state': self._use_state()}
6667
last_event_state = json.dumps(event)
6768
if self._last_event_state != last_event_state:
68-
await self.publish_event(event)
69+
self.publish_event(event)
6970
self._last_event_state = last_event_state
7071
threading.Timer(1.0, self._publish_state).start()
7172

src/talkops/readme.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
class Readme:
66
def __init__(self, getter):
77
self._getter = getter
8-
time.sleep(0.5)
98
self._generate()
109

1110
def _generate(self):

src/talkops/subscriber.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import asyncio
12
import json
3+
import os
4+
import re
5+
import requests
26
import sseclient
7+
import sys
38
import threading
4-
import requests
5-
import re
9+
import time
610
from urllib.parse import quote
7-
import os
811

912
class Subscriber:
1013
def __init__(self, use_config):
@@ -51,12 +54,11 @@ def _subscribe(self):
5154
self._on_event(json.loads(event.data))
5255

5356
except Exception as e:
54-
print(f"Error in subscriber: {e}")
57+
print(f"Error in subscriber: {e}", file=sys.stderr)
5558
if self._running:
56-
import time
5759
time.sleep(5)
5860

59-
async def _on_event(self, event):
61+
def _on_event(self, event):
6062
config = self._use_config()
6163
if event['type'] == 'ping':
6264
config['publisher'].on_ping()
@@ -65,17 +67,18 @@ async def _on_event(self, event):
6567
for fn in config['functions']:
6668
if fn.__name__ != event['name']:
6769
continue
68-
match = re.match(r'\(([^)]*)\)', fn.__code__.co_varnames[:fn.__code__.co_argcount])
69-
arguments_list = []
70-
if match:
71-
param_names = [p.strip() for p in match.group(1).split(',')]
72-
arguments_list = [
73-
event['args'].get(name) or event['defaultArgs'].get(name)
74-
for name in param_names
75-
]
76-
event['output'] = await fn(*arguments_list)
70+
arguments_list = [
71+
event['args'].get(name) or event['defaultArgs'].get(name)
72+
for name in fn.__code__.co_varnames
73+
]
74+
output = fn(*arguments_list)
75+
if asyncio.iscoroutine(output):
76+
output = asyncio.run(output)
77+
event['output'] = output
7778
config['publisher'].publish_event(event)
7879
return
80+
print(f'Function {fn.__name__} not found.', file=sys.stderr)
81+
return
7982
if event['type'] == 'boot':
8083
for name, value in event['parameters'].items():
8184
for parameter in config['parameters']:
@@ -94,13 +97,11 @@ async def _on_event(self, event):
9497
return
9598
if event['type'] in self._get_event_types() and event['type'] in config['callbacks']:
9699
callback = config['callbacks'][event['type']]
97-
match = re.match(r'\(([^)]*)\)', callback.__code__.co_varnames[:callback.__code__.co_argcount])
98-
arguments_list = []
99-
if match:
100-
param_names = [p.strip() for p in match.group(1).split(',')]
101-
arguments_list = [event['args'].get(name) for name in param_names]
102-
await callback(*arguments_list)
100+
arguments_list = [event['args'].get(name) for name in callback.__code__.co_varnames]
101+
output = callback(*arguments_list)
102+
if asyncio.iscoroutine(output):
103+
output = asyncio.run(output)
103104

104105
def _get_event_types(self):
105-
with open(os.path.join(os.path.dirname(__file__), 'event_types.json'), 'r') as f:
106+
with open(os.path.join(os.path.dirname(__file__), 'event-types.json'), 'r') as f:
106107
return json.load(f)

0 commit comments

Comments
 (0)