solana-agent 20.1.2__py3-none-any.whl → 31.4.0__py3-none-any.whl
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.
- solana_agent/__init__.py +10 -5
- solana_agent/adapters/ffmpeg_transcoder.py +375 -0
- solana_agent/adapters/mongodb_adapter.py +15 -2
- solana_agent/adapters/openai_adapter.py +679 -0
- solana_agent/adapters/openai_realtime_ws.py +1813 -0
- solana_agent/adapters/pinecone_adapter.py +543 -0
- solana_agent/cli.py +128 -0
- solana_agent/client/solana_agent.py +180 -20
- solana_agent/domains/agent.py +13 -13
- solana_agent/domains/routing.py +18 -8
- solana_agent/factories/agent_factory.py +239 -38
- solana_agent/guardrails/pii.py +107 -0
- solana_agent/interfaces/client/client.py +95 -12
- solana_agent/interfaces/guardrails/guardrails.py +26 -0
- solana_agent/interfaces/plugins/plugins.py +2 -1
- solana_agent/interfaces/providers/__init__.py +0 -0
- solana_agent/interfaces/providers/audio.py +40 -0
- solana_agent/interfaces/providers/data_storage.py +9 -2
- solana_agent/interfaces/providers/llm.py +86 -9
- solana_agent/interfaces/providers/memory.py +13 -1
- solana_agent/interfaces/providers/realtime.py +212 -0
- solana_agent/interfaces/providers/vector_storage.py +53 -0
- solana_agent/interfaces/services/agent.py +27 -12
- solana_agent/interfaces/services/knowledge_base.py +59 -0
- solana_agent/interfaces/services/query.py +41 -8
- solana_agent/interfaces/services/routing.py +0 -1
- solana_agent/plugins/manager.py +37 -16
- solana_agent/plugins/registry.py +34 -19
- solana_agent/plugins/tools/__init__.py +0 -5
- solana_agent/plugins/tools/auto_tool.py +1 -0
- solana_agent/repositories/memory.py +332 -111
- solana_agent/services/__init__.py +1 -1
- solana_agent/services/agent.py +390 -241
- solana_agent/services/knowledge_base.py +768 -0
- solana_agent/services/query.py +1858 -153
- solana_agent/services/realtime.py +626 -0
- solana_agent/services/routing.py +104 -51
- solana_agent-31.4.0.dist-info/METADATA +1070 -0
- solana_agent-31.4.0.dist-info/RECORD +49 -0
- {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info}/WHEEL +1 -1
- solana_agent-31.4.0.dist-info/entry_points.txt +3 -0
- solana_agent/adapters/llm_adapter.py +0 -160
- solana_agent-20.1.2.dist-info/METADATA +0 -464
- solana_agent-20.1.2.dist-info/RECORD +0 -35
- {solana_agent-20.1.2.dist-info → solana_agent-31.4.0.dist-info/licenses}/LICENSE +0 -0
solana_agent/__init__.py
CHANGED
|
@@ -5,8 +5,6 @@ This package provides a modular framework for building AI agent systems with
|
|
|
5
5
|
multiple specialized agents, memory management, and conversation routing.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "14.0.0" # Update with your actual version
|
|
9
|
-
|
|
10
8
|
# Client interface (main entry point)
|
|
11
9
|
from solana_agent.client.solana_agent import SolanaAgent
|
|
12
10
|
|
|
@@ -16,18 +14,25 @@ from solana_agent.factories.agent_factory import SolanaAgentFactory
|
|
|
16
14
|
# Useful tools and utilities
|
|
17
15
|
from solana_agent.plugins.manager import PluginManager
|
|
18
16
|
from solana_agent.plugins.registry import ToolRegistry
|
|
19
|
-
from solana_agent.plugins.tools import AutoTool
|
|
17
|
+
from solana_agent.plugins.tools.auto_tool import AutoTool
|
|
18
|
+
from solana_agent.interfaces.plugins.plugins import Tool
|
|
19
|
+
from solana_agent.interfaces.guardrails.guardrails import (
|
|
20
|
+
InputGuardrail,
|
|
21
|
+
OutputGuardrail,
|
|
22
|
+
)
|
|
20
23
|
|
|
21
24
|
# Package metadata
|
|
22
25
|
__all__ = [
|
|
23
26
|
# Main client interfaces
|
|
24
27
|
"SolanaAgent",
|
|
25
|
-
|
|
26
28
|
# Factories
|
|
27
29
|
"SolanaAgentFactory",
|
|
28
|
-
|
|
29
30
|
# Tools
|
|
30
31
|
"PluginManager",
|
|
31
32
|
"ToolRegistry",
|
|
32
33
|
"AutoTool",
|
|
34
|
+
"Tool",
|
|
35
|
+
# Guardrails
|
|
36
|
+
"InputGuardrail",
|
|
37
|
+
"OutputGuardrail",
|
|
33
38
|
]
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import logging
|
|
6
|
+
from typing import List, AsyncGenerator
|
|
7
|
+
import tempfile
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from solana_agent.interfaces.providers.audio import AudioTranscoder
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FFmpegTranscoder(AudioTranscoder):
|
|
16
|
+
"""FFmpeg-based transcoder. Requires 'ffmpeg' binary in PATH.
|
|
17
|
+
|
|
18
|
+
This uses subprocess to stream bytes through ffmpeg for encode/decode.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
async def _run_ffmpeg(
|
|
22
|
+
self, args: List[str], data: bytes
|
|
23
|
+
) -> bytes: # pragma: no cover
|
|
24
|
+
logger.info("FFmpeg: starting process args=%s, input_len=%d", args, len(data))
|
|
25
|
+
proc = await asyncio.create_subprocess_exec(
|
|
26
|
+
"ffmpeg",
|
|
27
|
+
*args,
|
|
28
|
+
stdin=asyncio.subprocess.PIPE,
|
|
29
|
+
stdout=asyncio.subprocess.PIPE,
|
|
30
|
+
stderr=asyncio.subprocess.PIPE,
|
|
31
|
+
)
|
|
32
|
+
stdout, stderr = await proc.communicate(input=data)
|
|
33
|
+
if proc.returncode != 0:
|
|
34
|
+
err = (stderr or b"").decode("utf-8", errors="ignore")
|
|
35
|
+
logger.error("FFmpeg failed (code=%s): %s", proc.returncode, err[:2000])
|
|
36
|
+
raise RuntimeError("ffmpeg failed to transcode audio")
|
|
37
|
+
logger.info("FFmpeg: finished successfully, output_len=%d", len(stdout or b""))
|
|
38
|
+
if stderr:
|
|
39
|
+
logger.debug(
|
|
40
|
+
"FFmpeg stderr: %s", stderr.decode("utf-8", errors="ignore")[:2000]
|
|
41
|
+
)
|
|
42
|
+
return stdout
|
|
43
|
+
|
|
44
|
+
async def to_pcm16( # pragma: no cover
|
|
45
|
+
self, audio_bytes: bytes, input_mime: str, rate_hz: int
|
|
46
|
+
) -> bytes:
|
|
47
|
+
"""Decode compressed audio to mono PCM16LE at rate_hz."""
|
|
48
|
+
logger.info(
|
|
49
|
+
"Transcode to PCM16: input_mime=%s, rate_hz=%d, input_len=%d",
|
|
50
|
+
input_mime,
|
|
51
|
+
rate_hz,
|
|
52
|
+
len(audio_bytes),
|
|
53
|
+
)
|
|
54
|
+
# iOS-recorded MP4/M4A often requires a seekable input for reliable demuxing.
|
|
55
|
+
# Decode from a temporary file instead of stdin for MP4/M4A.
|
|
56
|
+
if input_mime in ("audio/mp4", "audio/m4a"):
|
|
57
|
+
suffix = ".m4a" if input_mime == "audio/m4a" else ".mp4"
|
|
58
|
+
tmp_path = None
|
|
59
|
+
try:
|
|
60
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
|
|
61
|
+
tmp_path = tf.name
|
|
62
|
+
tf.write(audio_bytes)
|
|
63
|
+
args = [
|
|
64
|
+
"-hide_banner",
|
|
65
|
+
"-loglevel",
|
|
66
|
+
"error",
|
|
67
|
+
"-i",
|
|
68
|
+
tmp_path,
|
|
69
|
+
"-vn", # ignore any video tracks
|
|
70
|
+
"-acodec",
|
|
71
|
+
"pcm_s16le",
|
|
72
|
+
"-ac",
|
|
73
|
+
"1",
|
|
74
|
+
"-ar",
|
|
75
|
+
str(rate_hz),
|
|
76
|
+
"-f",
|
|
77
|
+
"s16le",
|
|
78
|
+
"pipe:1",
|
|
79
|
+
]
|
|
80
|
+
out = await self._run_ffmpeg(args, b"")
|
|
81
|
+
logger.info(
|
|
82
|
+
"Transcoded (MP4/M4A temp-file) to PCM16: output_len=%d", len(out)
|
|
83
|
+
)
|
|
84
|
+
return out
|
|
85
|
+
finally:
|
|
86
|
+
if tmp_path:
|
|
87
|
+
with contextlib.suppress(Exception):
|
|
88
|
+
os.remove(tmp_path)
|
|
89
|
+
|
|
90
|
+
# For other formats, prefer a format hint when helpful and decode from stdin.
|
|
91
|
+
hinted_format = None
|
|
92
|
+
if input_mime in ("audio/aac",):
|
|
93
|
+
# Raw AAC is typically in ADTS stream format
|
|
94
|
+
hinted_format = "adts"
|
|
95
|
+
elif input_mime in ("audio/ogg", "audio/webm"):
|
|
96
|
+
hinted_format = None # container detection is decent here
|
|
97
|
+
elif input_mime in ("audio/wav", "audio/x-wav"):
|
|
98
|
+
hinted_format = "wav"
|
|
99
|
+
|
|
100
|
+
args = [
|
|
101
|
+
"-hide_banner",
|
|
102
|
+
"-loglevel",
|
|
103
|
+
"error",
|
|
104
|
+
]
|
|
105
|
+
if hinted_format:
|
|
106
|
+
args += ["-f", hinted_format]
|
|
107
|
+
args += [
|
|
108
|
+
"-i",
|
|
109
|
+
"pipe:0",
|
|
110
|
+
"-acodec",
|
|
111
|
+
"pcm_s16le",
|
|
112
|
+
"-ac",
|
|
113
|
+
"1",
|
|
114
|
+
"-ar",
|
|
115
|
+
str(rate_hz),
|
|
116
|
+
"-f",
|
|
117
|
+
"s16le",
|
|
118
|
+
"pipe:1",
|
|
119
|
+
]
|
|
120
|
+
out = await self._run_ffmpeg(args, audio_bytes)
|
|
121
|
+
logger.info("Transcoded to PCM16: output_len=%d", len(out))
|
|
122
|
+
return out
|
|
123
|
+
|
|
124
|
+
async def from_pcm16( # pragma: no cover
|
|
125
|
+
self, pcm16_bytes: bytes, output_mime: str, rate_hz: int
|
|
126
|
+
) -> bytes:
|
|
127
|
+
"""Encode PCM16LE to desired format (AAC ADTS, fragmented MP4, or MP3)."""
|
|
128
|
+
logger.info(
|
|
129
|
+
"Encode from PCM16: output_mime=%s, rate_hz=%d, input_len=%d",
|
|
130
|
+
output_mime,
|
|
131
|
+
rate_hz,
|
|
132
|
+
len(pcm16_bytes),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if output_mime in ("audio/mpeg", "audio/mp3"):
|
|
136
|
+
# Encode to MP3 (often better streaming compatibility on mobile)
|
|
137
|
+
args = [
|
|
138
|
+
"-hide_banner",
|
|
139
|
+
"-loglevel",
|
|
140
|
+
"error",
|
|
141
|
+
"-f",
|
|
142
|
+
"s16le",
|
|
143
|
+
"-ac",
|
|
144
|
+
"1",
|
|
145
|
+
"-ar",
|
|
146
|
+
str(rate_hz),
|
|
147
|
+
"-i",
|
|
148
|
+
"pipe:0",
|
|
149
|
+
"-c:a",
|
|
150
|
+
"libmp3lame",
|
|
151
|
+
"-b:a",
|
|
152
|
+
"128k",
|
|
153
|
+
"-f",
|
|
154
|
+
"mp3",
|
|
155
|
+
"pipe:1",
|
|
156
|
+
]
|
|
157
|
+
out = await self._run_ffmpeg(args, pcm16_bytes)
|
|
158
|
+
logger.info(
|
|
159
|
+
"Encoded from PCM16 to %s: output_len=%d", output_mime, len(out)
|
|
160
|
+
)
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
if output_mime in ("audio/aac",):
|
|
164
|
+
# Encode to AAC in ADTS stream; good for streaming over sockets/HTTP chunked
|
|
165
|
+
args = [
|
|
166
|
+
"-hide_banner",
|
|
167
|
+
"-loglevel",
|
|
168
|
+
"error",
|
|
169
|
+
"-f",
|
|
170
|
+
"s16le",
|
|
171
|
+
"-ac",
|
|
172
|
+
"1",
|
|
173
|
+
"-ar",
|
|
174
|
+
str(rate_hz),
|
|
175
|
+
"-i",
|
|
176
|
+
"pipe:0",
|
|
177
|
+
"-c:a",
|
|
178
|
+
"aac",
|
|
179
|
+
"-b:a",
|
|
180
|
+
"96k",
|
|
181
|
+
"-f",
|
|
182
|
+
"adts",
|
|
183
|
+
"pipe:1",
|
|
184
|
+
]
|
|
185
|
+
out = await self._run_ffmpeg(args, pcm16_bytes)
|
|
186
|
+
logger.info(
|
|
187
|
+
"Encoded from PCM16 to %s: output_len=%d", output_mime, len(out)
|
|
188
|
+
)
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
if output_mime in ("audio/mp4", "audio/m4a"):
|
|
192
|
+
# Encode to fragmented MP4 (fMP4) with AAC for better iOS compatibility
|
|
193
|
+
# For streaming, write an initial moov and fragment over stdout.
|
|
194
|
+
args = [
|
|
195
|
+
"-hide_banner",
|
|
196
|
+
"-loglevel",
|
|
197
|
+
"error",
|
|
198
|
+
"-f",
|
|
199
|
+
"s16le",
|
|
200
|
+
"-ac",
|
|
201
|
+
"1",
|
|
202
|
+
"-ar",
|
|
203
|
+
str(rate_hz),
|
|
204
|
+
"-i",
|
|
205
|
+
"pipe:0",
|
|
206
|
+
"-c:a",
|
|
207
|
+
"aac",
|
|
208
|
+
"-b:a",
|
|
209
|
+
"96k",
|
|
210
|
+
"-movflags",
|
|
211
|
+
"+frag_keyframe+empty_moov",
|
|
212
|
+
"-f",
|
|
213
|
+
"mp4",
|
|
214
|
+
"pipe:1",
|
|
215
|
+
]
|
|
216
|
+
out = await self._run_ffmpeg(args, pcm16_bytes)
|
|
217
|
+
logger.info(
|
|
218
|
+
"Encoded from PCM16 to %s (fMP4): output_len=%d", output_mime, len(out)
|
|
219
|
+
)
|
|
220
|
+
return out
|
|
221
|
+
|
|
222
|
+
# Default: passthrough
|
|
223
|
+
logger.info("Encode passthrough (no change), output_len=%d", len(pcm16_bytes))
|
|
224
|
+
return pcm16_bytes
|
|
225
|
+
|
|
226
|
+
async def stream_from_pcm16( # pragma: no cover
|
|
227
|
+
self,
|
|
228
|
+
pcm_iter: AsyncGenerator[bytes, None],
|
|
229
|
+
output_mime: str,
|
|
230
|
+
rate_hz: int,
|
|
231
|
+
read_chunk_size: int = 4096,
|
|
232
|
+
) -> AsyncGenerator[bytes, None]:
|
|
233
|
+
"""Start a single continuous encoder and stream encoded audio chunks.
|
|
234
|
+
|
|
235
|
+
- Launches one ffmpeg subprocess for the entire response.
|
|
236
|
+
- Feeds PCM16LE mono bytes from pcm_iter into stdin.
|
|
237
|
+
- Yields encoded bytes from stdout as they become available.
|
|
238
|
+
"""
|
|
239
|
+
if output_mime in ("audio/mpeg", "audio/mp3"):
|
|
240
|
+
args = [
|
|
241
|
+
"-hide_banner",
|
|
242
|
+
"-loglevel",
|
|
243
|
+
"error",
|
|
244
|
+
"-f",
|
|
245
|
+
"s16le",
|
|
246
|
+
"-ac",
|
|
247
|
+
"1",
|
|
248
|
+
"-ar",
|
|
249
|
+
str(rate_hz),
|
|
250
|
+
"-i",
|
|
251
|
+
"pipe:0",
|
|
252
|
+
"-c:a",
|
|
253
|
+
"libmp3lame",
|
|
254
|
+
"-b:a",
|
|
255
|
+
"128k",
|
|
256
|
+
"-f",
|
|
257
|
+
"mp3",
|
|
258
|
+
"pipe:1",
|
|
259
|
+
]
|
|
260
|
+
elif output_mime in ("audio/aac",):
|
|
261
|
+
args = [
|
|
262
|
+
"-hide_banner",
|
|
263
|
+
"-loglevel",
|
|
264
|
+
"error",
|
|
265
|
+
"-f",
|
|
266
|
+
"s16le",
|
|
267
|
+
"-ac",
|
|
268
|
+
"1",
|
|
269
|
+
"-ar",
|
|
270
|
+
str(rate_hz),
|
|
271
|
+
"-i",
|
|
272
|
+
"pipe:0",
|
|
273
|
+
"-c:a",
|
|
274
|
+
"aac",
|
|
275
|
+
"-b:a",
|
|
276
|
+
"96k",
|
|
277
|
+
"-f",
|
|
278
|
+
"adts",
|
|
279
|
+
"pipe:1",
|
|
280
|
+
]
|
|
281
|
+
elif output_mime in ("audio/mp4", "audio/m4a"):
|
|
282
|
+
args = [
|
|
283
|
+
"-hide_banner",
|
|
284
|
+
"-loglevel",
|
|
285
|
+
"error",
|
|
286
|
+
"-f",
|
|
287
|
+
"s16le",
|
|
288
|
+
"-ac",
|
|
289
|
+
"1",
|
|
290
|
+
"-ar",
|
|
291
|
+
str(rate_hz),
|
|
292
|
+
"-i",
|
|
293
|
+
"pipe:0",
|
|
294
|
+
"-c:a",
|
|
295
|
+
"aac",
|
|
296
|
+
"-b:a",
|
|
297
|
+
"96k",
|
|
298
|
+
"-movflags",
|
|
299
|
+
"+frag_keyframe+empty_moov",
|
|
300
|
+
"-f",
|
|
301
|
+
"mp4",
|
|
302
|
+
"pipe:1",
|
|
303
|
+
]
|
|
304
|
+
else:
|
|
305
|
+
# Passthrough streaming: just yield input
|
|
306
|
+
async for chunk in pcm_iter:
|
|
307
|
+
yield chunk
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
logger.info("FFmpeg(stream): starting args=%s", args)
|
|
311
|
+
proc = await asyncio.create_subprocess_exec(
|
|
312
|
+
"ffmpeg",
|
|
313
|
+
*args,
|
|
314
|
+
stdin=asyncio.subprocess.PIPE,
|
|
315
|
+
stdout=asyncio.subprocess.PIPE,
|
|
316
|
+
stderr=asyncio.subprocess.PIPE,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
assert proc.stdin is not None and proc.stdout is not None
|
|
320
|
+
|
|
321
|
+
async def _writer():
|
|
322
|
+
try:
|
|
323
|
+
async for pcm in pcm_iter:
|
|
324
|
+
if not pcm:
|
|
325
|
+
continue
|
|
326
|
+
proc.stdin.write(pcm)
|
|
327
|
+
# Backpressure
|
|
328
|
+
await proc.stdin.drain()
|
|
329
|
+
except asyncio.CancelledError:
|
|
330
|
+
# Swallow cancellation; stdin will be closed below.
|
|
331
|
+
pass
|
|
332
|
+
except Exception as e:
|
|
333
|
+
logger.debug("FFmpeg(stream) writer error: %s", str(e))
|
|
334
|
+
finally:
|
|
335
|
+
with contextlib.suppress(Exception):
|
|
336
|
+
proc.stdin.close()
|
|
337
|
+
|
|
338
|
+
writer_task = asyncio.create_task(_writer())
|
|
339
|
+
|
|
340
|
+
buf = bytearray()
|
|
341
|
+
try:
|
|
342
|
+
while True:
|
|
343
|
+
data = await proc.stdout.read(read_chunk_size)
|
|
344
|
+
if not data:
|
|
345
|
+
break
|
|
346
|
+
buf.extend(data)
|
|
347
|
+
# Emit fixed-size chunks even if read returns a larger blob
|
|
348
|
+
while len(buf) >= read_chunk_size:
|
|
349
|
+
yield bytes(buf[:read_chunk_size])
|
|
350
|
+
del buf[:read_chunk_size]
|
|
351
|
+
# Flush any remainder
|
|
352
|
+
if buf:
|
|
353
|
+
yield bytes(buf)
|
|
354
|
+
finally:
|
|
355
|
+
# Ensure writer is done
|
|
356
|
+
if not writer_task.done():
|
|
357
|
+
with contextlib.suppress(Exception):
|
|
358
|
+
writer_task.cancel()
|
|
359
|
+
try:
|
|
360
|
+
await writer_task
|
|
361
|
+
except asyncio.CancelledError:
|
|
362
|
+
pass
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
# Drain remaining stderr and check return code
|
|
366
|
+
try:
|
|
367
|
+
stderr = await proc.stderr.read() if proc.stderr else b""
|
|
368
|
+
code = await proc.wait()
|
|
369
|
+
if code != 0:
|
|
370
|
+
err = (stderr or b"").decode("utf-8", errors="ignore")
|
|
371
|
+
logger.error(
|
|
372
|
+
"FFmpeg(stream) failed (code=%s): %s", code, err[:2000]
|
|
373
|
+
)
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
@@ -3,6 +3,7 @@ MongoDB adapter for the Solana Agent system.
|
|
|
3
3
|
|
|
4
4
|
This adapter implements the DataStorageProvider interface for MongoDB.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import uuid
|
|
7
8
|
from typing import Dict, List, Tuple, Optional
|
|
8
9
|
|
|
@@ -31,6 +32,16 @@ class MongoDBAdapter(DataStorageProvider):
|
|
|
31
32
|
self.db[collection].insert_one(document)
|
|
32
33
|
return document["_id"]
|
|
33
34
|
|
|
35
|
+
def insert_many(self, collection: str, documents: List[Dict]) -> List[str]:
|
|
36
|
+
for document in documents:
|
|
37
|
+
if "_id" not in document:
|
|
38
|
+
document["_id"] = str(uuid.uuid4())
|
|
39
|
+
result = self.db[collection].insert_many(documents)
|
|
40
|
+
return [str(doc_id) for doc_id in result.inserted_ids]
|
|
41
|
+
|
|
42
|
+
def delete_many(self, collection: str, query: Dict):
|
|
43
|
+
return self.db[collection].delete_many(query)
|
|
44
|
+
|
|
34
45
|
def find_one(self, collection: str, query: Dict) -> Optional[Dict]:
|
|
35
46
|
return self.db[collection].find_one(query)
|
|
36
47
|
|
|
@@ -40,7 +51,7 @@ class MongoDBAdapter(DataStorageProvider):
|
|
|
40
51
|
query: Dict,
|
|
41
52
|
sort: Optional[List[Tuple]] = None,
|
|
42
53
|
limit: int = 0,
|
|
43
|
-
skip: int = 0
|
|
54
|
+
skip: int = 0,
|
|
44
55
|
) -> List[Dict]:
|
|
45
56
|
cursor = self.db[collection].find(query)
|
|
46
57
|
if sort:
|
|
@@ -51,7 +62,9 @@ class MongoDBAdapter(DataStorageProvider):
|
|
|
51
62
|
cursor = cursor.skip(skip)
|
|
52
63
|
return list(cursor)
|
|
53
64
|
|
|
54
|
-
def update_one(
|
|
65
|
+
def update_one(
|
|
66
|
+
self, collection: str, query: Dict, update: Dict, upsert: bool = False
|
|
67
|
+
) -> bool:
|
|
55
68
|
result = self.db[collection].update_one(query, update, upsert=upsert)
|
|
56
69
|
return result.modified_count > 0 or (upsert and result.upserted_id is not None)
|
|
57
70
|
|