wirexaapi 1.0.1__tar.gz → 1.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.
- {wirexaapi-1.0.1 → wirexaapi-1.1.0}/PKG-INFO +34 -2
- wirexaapi-1.1.0/README.md +96 -0
- {wirexaapi-1.0.1 → wirexaapi-1.1.0}/pyproject.toml +2 -2
- wirexaapi-1.1.0/wirexaapi/__init__.py +38 -0
- wirexaapi-1.1.0/wirexaapi/db.py +665 -0
- {wirexaapi-1.0.1 → wirexaapi-1.1.0}/wirexaapi.egg-info/PKG-INFO +34 -2
- {wirexaapi-1.0.1 → wirexaapi-1.1.0}/wirexaapi.egg-info/SOURCES.txt +1 -0
- wirexaapi-1.0.1/README.md +0 -64
- wirexaapi-1.0.1/wirexaapi/__init__.py +0 -23
- {wirexaapi-1.0.1 → wirexaapi-1.1.0}/setup.cfg +0 -0
- {wirexaapi-1.0.1 → wirexaapi-1.1.0}/wirexaapi/rest/__init__.py +0 -0
- {wirexaapi-1.0.1 → wirexaapi-1.1.0}/wirexaapi.egg-info/dependency_links.txt +0 -0
- {wirexaapi-1.0.1 → wirexaapi-1.1.0}/wirexaapi.egg-info/requires.txt +0 -0
- {wirexaapi-1.0.1 → wirexaapi-1.1.0}/wirexaapi.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wirexaapi
|
|
3
|
-
Version: 1.0
|
|
4
|
-
Summary: Wirexa API SDK - SignalWire/Twilio compatible
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Wirexa API SDK - voice API client (SignalWire/Twilio compatible) and Wirexa DB document database client
|
|
5
5
|
Author-email: Wirexa Team <dev@wirexa.io>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/wirexa/wirexa-api
|
|
@@ -84,3 +84,35 @@ The rest of your code stays the same!
|
|
|
84
84
|
## License
|
|
85
85
|
|
|
86
86
|
MIT
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## Wirexa DB (base de datos de documentos)
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from wirexaapi import MongoClient
|
|
93
|
+
|
|
94
|
+
client = MongoClient(
|
|
95
|
+
"wirexaapi+srv://usuario:password@cluster0-abc1234.wirexaapi.com/?appName=cluster0",
|
|
96
|
+
serverSelectionTimeoutMS=5000,
|
|
97
|
+
connectTimeoutMS=5000,
|
|
98
|
+
socketTimeoutMS=20000,
|
|
99
|
+
)
|
|
100
|
+
db = client["miapp"]
|
|
101
|
+
users = db["users"] # las colecciones se crean solas
|
|
102
|
+
|
|
103
|
+
users.insert_one({"name": "Ana", "age": 30})
|
|
104
|
+
users.find_one({"name": "Ana"})
|
|
105
|
+
for u in users.find({"age": {"$gt": 25}}).sort("name").limit(10):
|
|
106
|
+
print(u)
|
|
107
|
+
users.update_one({"name": "Ana"}, {"$set": {"age": 31}})
|
|
108
|
+
users.create_index("email", unique=True)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Soporta filtros (`$eq $ne $gt $gte $lt $lte $in $nin $exists $regex $and $or $nor $not $size $all $elemMatch`),
|
|
112
|
+
updates (`$set $unset $inc $mul $min $max $push $addToSet $pull $pop $rename $currentDate $setOnInsert`), upserts,
|
|
113
|
+
`find_one_and_*`, índices (incluidos únicos), `distinct`, `count_documents` y `aggregate`
|
|
114
|
+
(`$match $sort $skip $limit $project $addFields $unset $unwind $group $count $lookup`).
|
|
115
|
+
|
|
116
|
+
Si tu backend corre en la misma VPS que el cluster, usa la URI local que da el panel
|
|
117
|
+
(`wirexaapi://...@127.0.0.1:27501/?cluster=<id>&tls=false`): ~1 ms por consulta.
|
|
118
|
+
Instala `wirexaapi[srv]` para resolver el registro SRV del cluster.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Wirexa API SDK
|
|
2
|
+
|
|
3
|
+
A production-ready Python SDK for Wirexa API - a self-hosted telephony platform compatible with SignalWire and Twilio REST API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install wirexaapi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from wirexaapi import Client
|
|
15
|
+
|
|
16
|
+
# Connect to your Wirexa API instance
|
|
17
|
+
client = Client(
|
|
18
|
+
project_id="your-project-id",
|
|
19
|
+
api_token="your-api-token",
|
|
20
|
+
signalwire_space_url="your-space.example.com:5000"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Make a call
|
|
24
|
+
call = client.calls.create(
|
|
25
|
+
to="+573001234567",
|
|
26
|
+
from_="+18005550100",
|
|
27
|
+
url="https://your-backend.com/xml",
|
|
28
|
+
method="POST",
|
|
29
|
+
timeout=28,
|
|
30
|
+
record=True,
|
|
31
|
+
status_callback="https://your-backend.com/status",
|
|
32
|
+
status_callback_event=["initiated", "ringing", "completed"],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
print(f"Call SID: {call.sid}")
|
|
36
|
+
print(f"Call Status: {call.status}")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Migration from SignalWire
|
|
40
|
+
|
|
41
|
+
If you're migrating from SignalWire SDK:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# Before
|
|
45
|
+
from signalwire.rest import Client as SignalWireClient
|
|
46
|
+
|
|
47
|
+
# After
|
|
48
|
+
from wirexaapi import Client as SignalWireClient
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The rest of your code stays the same!
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- Full SignalWire/Twilio API compatibility
|
|
56
|
+
- Call creation, retrieval, and listing
|
|
57
|
+
- Machine Detection (AMD)
|
|
58
|
+
- Recording
|
|
59
|
+
- Status callbacks
|
|
60
|
+
- Async AMD support
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## Wirexa DB (base de datos de documentos)
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from wirexaapi import MongoClient
|
|
71
|
+
|
|
72
|
+
client = MongoClient(
|
|
73
|
+
"wirexaapi+srv://usuario:password@cluster0-abc1234.wirexaapi.com/?appName=cluster0",
|
|
74
|
+
serverSelectionTimeoutMS=5000,
|
|
75
|
+
connectTimeoutMS=5000,
|
|
76
|
+
socketTimeoutMS=20000,
|
|
77
|
+
)
|
|
78
|
+
db = client["miapp"]
|
|
79
|
+
users = db["users"] # las colecciones se crean solas
|
|
80
|
+
|
|
81
|
+
users.insert_one({"name": "Ana", "age": 30})
|
|
82
|
+
users.find_one({"name": "Ana"})
|
|
83
|
+
for u in users.find({"age": {"$gt": 25}}).sort("name").limit(10):
|
|
84
|
+
print(u)
|
|
85
|
+
users.update_one({"name": "Ana"}, {"$set": {"age": 31}})
|
|
86
|
+
users.create_index("email", unique=True)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Soporta filtros (`$eq $ne $gt $gte $lt $lte $in $nin $exists $regex $and $or $nor $not $size $all $elemMatch`),
|
|
90
|
+
updates (`$set $unset $inc $mul $min $max $push $addToSet $pull $pop $rename $currentDate $setOnInsert`), upserts,
|
|
91
|
+
`find_one_and_*`, índices (incluidos únicos), `distinct`, `count_documents` y `aggregate`
|
|
92
|
+
(`$match $sort $skip $limit $project $addFields $unset $unwind $group $count $lookup`).
|
|
93
|
+
|
|
94
|
+
Si tu backend corre en la misma VPS que el cluster, usa la URI local que da el panel
|
|
95
|
+
(`wirexaapi://...@127.0.0.1:27501/?cluster=<id>&tls=false`): ~1 ms por consulta.
|
|
96
|
+
Instala `wirexaapi[srv]` para resolver el registro SRV del cluster.
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wirexaapi"
|
|
7
|
-
version = "1.0
|
|
8
|
-
description = "Wirexa API SDK - SignalWire/Twilio compatible
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "Wirexa API SDK - voice API client (SignalWire/Twilio compatible) and Wirexa DB document database client"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
11
11
|
authors = [
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wirexa API SDK - SDK compatible con la API de voz WirexaAPI.
|
|
3
|
+
|
|
4
|
+
CREDENCIALES (las obtienes del dashboard):
|
|
5
|
+
space_url = "your-space.example.com:5000"
|
|
6
|
+
project_id = "your-project-id"
|
|
7
|
+
api_token = "your-api-token"
|
|
8
|
+
|
|
9
|
+
USO:
|
|
10
|
+
|
|
11
|
+
ANTES (SignalWire original):
|
|
12
|
+
from signalwire.rest import Client
|
|
13
|
+
client = Client(project_id, api_token, signalwire_space_url=space_url)
|
|
14
|
+
|
|
15
|
+
DESPUÉS (Wirexa API):
|
|
16
|
+
from wirexaapi import Client
|
|
17
|
+
client = Client(project_id, api_token, signalwire_space_url=space_url)
|
|
18
|
+
|
|
19
|
+
El resto del código NO cambia.
|
|
20
|
+
|
|
21
|
+
BASE DE DATOS (Wirexa DB):
|
|
22
|
+
|
|
23
|
+
from wirexaapi import MongoClient
|
|
24
|
+
client = MongoClient("wirexaapi+srv://user:pass@cluster0-abc1234.wirexaapi.com/")
|
|
25
|
+
client["miapp"]["users"].insert_one({"name": "Ana"})
|
|
26
|
+
"""
|
|
27
|
+
from wirexaapi.rest import Client
|
|
28
|
+
from wirexaapi.db import (
|
|
29
|
+
WirexaClient, MongoClient, ASCENDING, DESCENDING, ReturnDocument, ObjectId,
|
|
30
|
+
WirexaDBError, OperationFailure, DuplicateKeyError, ConnectionFailure,
|
|
31
|
+
ServerSelectionTimeoutError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Client", "WirexaClient", "MongoClient", "ASCENDING", "DESCENDING", "ReturnDocument", "ObjectId",
|
|
36
|
+
"WirexaDBError", "OperationFailure", "DuplicateKeyError", "ConnectionFailure",
|
|
37
|
+
"ServerSelectionTimeoutError",
|
|
38
|
+
]
|
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wirexa DB - cliente de la base de datos de documentos de Wirexa.
|
|
3
|
+
|
|
4
|
+
USO:
|
|
5
|
+
|
|
6
|
+
from wirexaapi import MongoClient # alias de WirexaClient
|
|
7
|
+
|
|
8
|
+
client = MongoClient(
|
|
9
|
+
"wirexaapi+srv://usuario:password@cluster0-abc1234.wirexaapi.com/?appName=cluster0",
|
|
10
|
+
serverSelectionTimeoutMS=5000,
|
|
11
|
+
connectTimeoutMS=5000,
|
|
12
|
+
socketTimeoutMS=20000,
|
|
13
|
+
)
|
|
14
|
+
db = client["miapp"]
|
|
15
|
+
users = db["users"] # o db.users; la colección nace sola
|
|
16
|
+
|
|
17
|
+
users.insert_one({"name": "Ana", "age": 30})
|
|
18
|
+
users.find_one({"name": "Ana"})
|
|
19
|
+
for u in users.find({"age": {"$gt": 25}}).sort("name").limit(10):
|
|
20
|
+
print(u)
|
|
21
|
+
users.update_one({"name": "Ana"}, {"$set": {"age": 31}})
|
|
22
|
+
users.create_index("email", unique=True)
|
|
23
|
+
|
|
24
|
+
Si tu backend corre en la misma VPS que el cluster usa la URI local
|
|
25
|
+
(wirexaapi://usuario:password@127.0.0.1:27501/?cluster=<id>&tls=false):
|
|
26
|
+
evita TLS y la red, y responde en ~1 ms.
|
|
27
|
+
"""
|
|
28
|
+
import base64
|
|
29
|
+
import datetime as _dt
|
|
30
|
+
import decimal
|
|
31
|
+
import http.client
|
|
32
|
+
import json
|
|
33
|
+
import re
|
|
34
|
+
import socket
|
|
35
|
+
import ssl
|
|
36
|
+
import threading
|
|
37
|
+
from urllib.parse import parse_qs, unquote, urlsplit
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"WirexaClient", "MongoClient", "Database", "Collection", "Cursor",
|
|
41
|
+
"ASCENDING", "DESCENDING", "ReturnDocument", "ObjectId",
|
|
42
|
+
"WirexaDBError", "PyMongoError", "OperationFailure", "DuplicateKeyError",
|
|
43
|
+
"ConnectionFailure", "ServerSelectionTimeoutError", "ConfigurationError",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
ASCENDING = 1
|
|
47
|
+
DESCENDING = -1
|
|
48
|
+
DEFAULT_PORT = 443 # vía nginx; pasa por cualquier firewall
|
|
49
|
+
LOCAL_PORT = 27501 # backends en la misma VPS (sin TLS ni red)
|
|
50
|
+
_PATH = "/_wdb/v1/op"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ReturnDocument:
|
|
54
|
+
BEFORE = False
|
|
55
|
+
AFTER = True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ObjectId(str):
|
|
59
|
+
"""Los _id generados por el servidor son cadenas de 24 hex (mismo esquema
|
|
60
|
+
que un ObjectId: ordenan por fecha). Esta clase existe por compatibilidad:
|
|
61
|
+
ObjectId("...") devuelve la misma cadena."""
|
|
62
|
+
|
|
63
|
+
def __new__(cls, oid=None):
|
|
64
|
+
if oid is None:
|
|
65
|
+
raise ValueError("ObjectId() sin argumento: deja que el servidor genere el _id")
|
|
66
|
+
return str.__new__(cls, str(oid))
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def generation_time(self):
|
|
70
|
+
return _dt.datetime.fromtimestamp(int(self[:8], 16), tz=_dt.timezone.utc)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------ errores
|
|
74
|
+
|
|
75
|
+
class WirexaDBError(Exception):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
PyMongoError = WirexaDBError # mismo nombre que pymongo para except existentes
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ConfigurationError(WirexaDBError):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ConnectionFailure(WirexaDBError):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ServerSelectionTimeoutError(ConnectionFailure):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OperationFailure(WirexaDBError):
|
|
95
|
+
def __init__(self, message, code=None, details=None):
|
|
96
|
+
super().__init__(message)
|
|
97
|
+
self.code = code
|
|
98
|
+
self.details = details or {}
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def code_name(self):
|
|
102
|
+
return self.details.get("codeName")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DuplicateKeyError(OperationFailure):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------ JSON extendido
|
|
110
|
+
|
|
111
|
+
def _encode(v):
|
|
112
|
+
if isinstance(v, dict):
|
|
113
|
+
return {str(k): _encode(x) for k, x in v.items()}
|
|
114
|
+
if isinstance(v, (list, tuple, set)):
|
|
115
|
+
return [_encode(x) for x in v]
|
|
116
|
+
if isinstance(v, _dt.datetime):
|
|
117
|
+
if v.tzinfo is None:
|
|
118
|
+
v = v.replace(tzinfo=_dt.timezone.utc)
|
|
119
|
+
v = v.astimezone(_dt.timezone.utc)
|
|
120
|
+
return {"$date": v.strftime("%Y-%m-%dT%H:%M:%S.") + "%03dZ" % (v.microsecond // 1000)}
|
|
121
|
+
if isinstance(v, _dt.date):
|
|
122
|
+
return _encode(_dt.datetime(v.year, v.month, v.day, tzinfo=_dt.timezone.utc))
|
|
123
|
+
if isinstance(v, (bytes, bytearray)):
|
|
124
|
+
return {"$binary": base64.b64encode(bytes(v)).decode()}
|
|
125
|
+
if isinstance(v, decimal.Decimal):
|
|
126
|
+
return {"$numberDecimal": str(v)}
|
|
127
|
+
if isinstance(v, re.Pattern):
|
|
128
|
+
opts = ""
|
|
129
|
+
if v.flags & re.IGNORECASE:
|
|
130
|
+
opts += "i"
|
|
131
|
+
if v.flags & re.MULTILINE:
|
|
132
|
+
opts += "m"
|
|
133
|
+
if v.flags & re.DOTALL:
|
|
134
|
+
opts += "s"
|
|
135
|
+
return {"$regex": v.pattern, "$options": opts}
|
|
136
|
+
return v
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _decode_hook(d):
|
|
140
|
+
if len(d) == 1:
|
|
141
|
+
if "$date" in d:
|
|
142
|
+
s = d["$date"]
|
|
143
|
+
try:
|
|
144
|
+
return _dt.datetime.strptime(s, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=_dt.timezone.utc)
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
return d
|
|
147
|
+
if "$binary" in d:
|
|
148
|
+
try:
|
|
149
|
+
return base64.b64decode(d["$binary"])
|
|
150
|
+
except Exception:
|
|
151
|
+
return d
|
|
152
|
+
if "$numberDecimal" in d:
|
|
153
|
+
return decimal.Decimal(d["$numberDecimal"])
|
|
154
|
+
return d
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _sort_spec(key_or_list, direction=None):
|
|
158
|
+
"""Acepta los mismos formatos que pymongo: "campo", ("campo", -1),
|
|
159
|
+
[("a", 1), ("b", -1)] o {"a": 1}."""
|
|
160
|
+
if key_or_list is None:
|
|
161
|
+
return None
|
|
162
|
+
if isinstance(key_or_list, str):
|
|
163
|
+
return [[key_or_list, direction if direction is not None else ASCENDING]]
|
|
164
|
+
if isinstance(key_or_list, dict):
|
|
165
|
+
return [[k, v] for k, v in key_or_list.items()]
|
|
166
|
+
if isinstance(key_or_list, tuple) and len(key_or_list) == 2 and isinstance(key_or_list[0], str) \
|
|
167
|
+
and isinstance(key_or_list[1], int):
|
|
168
|
+
return [list(key_or_list)]
|
|
169
|
+
out = []
|
|
170
|
+
for item in key_or_list:
|
|
171
|
+
if isinstance(item, str):
|
|
172
|
+
out.append([item, ASCENDING])
|
|
173
|
+
else:
|
|
174
|
+
out.append([item[0], item[1]])
|
|
175
|
+
return out
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _projection(p):
|
|
179
|
+
if p is None:
|
|
180
|
+
return None
|
|
181
|
+
if isinstance(p, (list, tuple)):
|
|
182
|
+
return {k: 1 for k in p}
|
|
183
|
+
return p
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------------ resultados
|
|
187
|
+
|
|
188
|
+
class InsertOneResult:
|
|
189
|
+
def __init__(self, inserted_id):
|
|
190
|
+
self.inserted_id = inserted_id
|
|
191
|
+
self.acknowledged = True
|
|
192
|
+
|
|
193
|
+
def __repr__(self):
|
|
194
|
+
return "InsertOneResult(%r)" % (self.inserted_id,)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class InsertManyResult:
|
|
198
|
+
def __init__(self, inserted_ids):
|
|
199
|
+
self.inserted_ids = inserted_ids
|
|
200
|
+
self.acknowledged = True
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class UpdateResult:
|
|
204
|
+
def __init__(self, r):
|
|
205
|
+
self.matched_count = r.get("matchedCount", 0)
|
|
206
|
+
self.modified_count = r.get("modifiedCount", 0)
|
|
207
|
+
self.upserted_id = r.get("upsertedId")
|
|
208
|
+
self.raw_result = r
|
|
209
|
+
self.acknowledged = True
|
|
210
|
+
|
|
211
|
+
def __repr__(self):
|
|
212
|
+
return "UpdateResult(matched=%d, modified=%d, upserted_id=%r)" % (
|
|
213
|
+
self.matched_count, self.modified_count, self.upserted_id)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class DeleteResult:
|
|
217
|
+
def __init__(self, r):
|
|
218
|
+
self.deleted_count = r.get("deletedCount", 0)
|
|
219
|
+
self.raw_result = r
|
|
220
|
+
self.acknowledged = True
|
|
221
|
+
|
|
222
|
+
def __repr__(self):
|
|
223
|
+
return "DeleteResult(deleted=%d)" % self.deleted_count
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ------------------------------------------------------------------ cliente
|
|
227
|
+
|
|
228
|
+
def _parse_uri(uri):
|
|
229
|
+
parts = urlsplit(uri)
|
|
230
|
+
scheme = parts.scheme.lower()
|
|
231
|
+
if scheme not in ("wirexaapi", "wirexaapi+srv", "wirexa", "wirexa+srv"):
|
|
232
|
+
raise ConfigurationError("la URI debe empezar con wirexaapi:// o wirexaapi+srv://")
|
|
233
|
+
if not parts.hostname:
|
|
234
|
+
raise ConfigurationError("la URI no tiene host")
|
|
235
|
+
q = {k: v[-1] for k, v in parse_qs(parts.query).items()}
|
|
236
|
+
host = parts.hostname
|
|
237
|
+
label = host.split(".")[0]
|
|
238
|
+
cluster = q.get("cluster") or (label.rsplit("-", 1)[1] if "-" in label else "")
|
|
239
|
+
if not cluster:
|
|
240
|
+
raise ConfigurationError("no se pudo deducir el id del cluster; añade ?cluster=<id>")
|
|
241
|
+
local = host in ("127.0.0.1", "localhost", "::1")
|
|
242
|
+
tls = q.get("tls", q.get("ssl", "false" if local else "true")).lower() != "false"
|
|
243
|
+
port = parts.port
|
|
244
|
+
if port is None:
|
|
245
|
+
port = LOCAL_PORT if local and not tls else DEFAULT_PORT
|
|
246
|
+
if scheme.endswith("+srv"):
|
|
247
|
+
port = _resolve_srv(host) or port
|
|
248
|
+
return {
|
|
249
|
+
"username": unquote(parts.username or ""),
|
|
250
|
+
"password": unquote(parts.password or ""),
|
|
251
|
+
"host": host, "port": port, "tls": tls, "cluster": cluster,
|
|
252
|
+
"appName": q.get("appName", ""), "options": q,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _resolve_srv(host):
|
|
257
|
+
"""Resuelve _wirexadb._tcp.<host> si dnspython está instalado; si no,
|
|
258
|
+
se usa el puerto por defecto (27500)."""
|
|
259
|
+
try:
|
|
260
|
+
import dns.resolver # type: ignore
|
|
261
|
+
except ImportError:
|
|
262
|
+
return None
|
|
263
|
+
try:
|
|
264
|
+
ans = dns.resolver.resolve("_wirexadb._tcp." + host, "SRV", lifetime=3)
|
|
265
|
+
best = sorted(ans, key=lambda r: (r.priority, -r.weight))[0]
|
|
266
|
+
return int(best.port)
|
|
267
|
+
except Exception:
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class WirexaClient:
|
|
272
|
+
"""Cliente de Wirexa DB. Thread-safe: una conexión keep-alive por hilo."""
|
|
273
|
+
|
|
274
|
+
def __init__(self, host=None, serverSelectionTimeoutMS=30000, connectTimeoutMS=20000,
|
|
275
|
+
socketTimeoutMS=None, maxPoolSize=100, connect=True, tlsCAFile=None, **kwargs):
|
|
276
|
+
if host is None:
|
|
277
|
+
raise ConfigurationError("falta la URI del cluster")
|
|
278
|
+
cfg = _parse_uri(host)
|
|
279
|
+
if not cfg["username"]:
|
|
280
|
+
raise ConfigurationError("la URI no incluye usuario y contraseña")
|
|
281
|
+
self._cfg = cfg
|
|
282
|
+
self._connect_timeout = min(connectTimeoutMS, serverSelectionTimeoutMS) / 1000.0
|
|
283
|
+
self._read_timeout = (socketTimeoutMS / 1000.0) if socketTimeoutMS else 60.0
|
|
284
|
+
self._ssl = ssl.create_default_context(cafile=tlsCAFile) if cfg["tls"] else None
|
|
285
|
+
token = base64.b64encode(("%s:%s" % (cfg["username"], cfg["password"])).encode()).decode()
|
|
286
|
+
self._headers = {
|
|
287
|
+
"Authorization": "Basic " + token,
|
|
288
|
+
"X-Wirexa-Cluster": cfg["cluster"],
|
|
289
|
+
"Content-Type": "application/json",
|
|
290
|
+
"User-Agent": "wirexaapi-py/db " + (cfg["appName"] or "-"),
|
|
291
|
+
}
|
|
292
|
+
self._local = threading.local()
|
|
293
|
+
self._all = []
|
|
294
|
+
self._lock = threading.Lock()
|
|
295
|
+
self._closed = False
|
|
296
|
+
self.address = (cfg["host"], cfg["port"])
|
|
297
|
+
|
|
298
|
+
def _conn(self):
|
|
299
|
+
c = getattr(self._local, "conn", None)
|
|
300
|
+
if c is None:
|
|
301
|
+
host, port = self.address
|
|
302
|
+
if self._ssl is not None:
|
|
303
|
+
c = http.client.HTTPSConnection(host, port, timeout=self._connect_timeout, context=self._ssl)
|
|
304
|
+
else:
|
|
305
|
+
c = http.client.HTTPConnection(host, port, timeout=self._connect_timeout)
|
|
306
|
+
self._local.conn = c
|
|
307
|
+
with self._lock:
|
|
308
|
+
self._all.append(c)
|
|
309
|
+
if c.sock is None:
|
|
310
|
+
try:
|
|
311
|
+
c.connect()
|
|
312
|
+
except socket.timeout as e:
|
|
313
|
+
raise ServerSelectionTimeoutError("%s:%d: %s" % (self.address + (e,)))
|
|
314
|
+
except OSError as e:
|
|
315
|
+
raise ConnectionFailure("%s:%d: %s" % (self.address + (e,)))
|
|
316
|
+
c.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
317
|
+
c.sock.settimeout(self._read_timeout)
|
|
318
|
+
return c
|
|
319
|
+
|
|
320
|
+
def _op(self, op, retry_safe=False, **fields):
|
|
321
|
+
if self._closed:
|
|
322
|
+
raise ConnectionFailure("el cliente está cerrado")
|
|
323
|
+
body = json.dumps(_encode(dict(op=op, **fields)), separators=(",", ":")).encode()
|
|
324
|
+
for attempt in (0, 1):
|
|
325
|
+
c = self._conn()
|
|
326
|
+
reused = getattr(c, "_wdb_used", False)
|
|
327
|
+
try:
|
|
328
|
+
c.request("POST", _PATH, body, self._headers)
|
|
329
|
+
r = c.getresponse()
|
|
330
|
+
status, raw = r.status, r.read()
|
|
331
|
+
c._wdb_used = True
|
|
332
|
+
break
|
|
333
|
+
except (http.client.RemoteDisconnected, ConnectionResetError, BrokenPipeError,
|
|
334
|
+
http.client.CannotSendRequest, http.client.ResponseNotReady) as e:
|
|
335
|
+
# Conexión keep-alive cerrada por el servidor mientras estaba
|
|
336
|
+
# ociosa: se reabre y se reintenta una sola vez.
|
|
337
|
+
c.close()
|
|
338
|
+
c._wdb_used = False
|
|
339
|
+
if attempt == 1 or not (reused or retry_safe):
|
|
340
|
+
raise ConnectionFailure("%s:%d: %s" % (self.address + (e,)))
|
|
341
|
+
except socket.timeout as e:
|
|
342
|
+
c.close()
|
|
343
|
+
raise ConnectionFailure("timeout esperando respuesta: %s" % e)
|
|
344
|
+
except OSError as e:
|
|
345
|
+
c.close()
|
|
346
|
+
raise ConnectionFailure("%s:%d: %s" % (self.address + (e,)))
|
|
347
|
+
try:
|
|
348
|
+
data = json.loads(raw, object_hook=_decode_hook)
|
|
349
|
+
except ValueError:
|
|
350
|
+
raise OperationFailure("respuesta inválida del servidor (HTTP %d)" % status)
|
|
351
|
+
if not data.get("ok"):
|
|
352
|
+
msg = data.get("errmsg", "error")
|
|
353
|
+
code = data.get("code")
|
|
354
|
+
if code == 11000:
|
|
355
|
+
raise DuplicateKeyError(msg, code, data)
|
|
356
|
+
raise OperationFailure(msg, code, data)
|
|
357
|
+
return data
|
|
358
|
+
|
|
359
|
+
def __getitem__(self, name):
|
|
360
|
+
return Database(self, name)
|
|
361
|
+
|
|
362
|
+
def __getattr__(self, name):
|
|
363
|
+
if name.startswith("_"):
|
|
364
|
+
raise AttributeError(name)
|
|
365
|
+
return Database(self, name)
|
|
366
|
+
|
|
367
|
+
def get_database(self, name=None, **kwargs):
|
|
368
|
+
return Database(self, name or "test")
|
|
369
|
+
|
|
370
|
+
def list_database_names(self):
|
|
371
|
+
return [d["name"] for d in self._op("listDatabases", True)["databases"]]
|
|
372
|
+
|
|
373
|
+
def drop_database(self, name):
|
|
374
|
+
if isinstance(name, Database):
|
|
375
|
+
name = name.name
|
|
376
|
+
self._op("dropDatabase", db=name)
|
|
377
|
+
|
|
378
|
+
def server_info(self):
|
|
379
|
+
self._op("ping", True)
|
|
380
|
+
return {"ok": 1.0, "version": "wirexadb-1"}
|
|
381
|
+
|
|
382
|
+
def close(self):
|
|
383
|
+
self._closed = True
|
|
384
|
+
with self._lock:
|
|
385
|
+
for c in self._all:
|
|
386
|
+
c.close()
|
|
387
|
+
self._all = []
|
|
388
|
+
|
|
389
|
+
def __enter__(self):
|
|
390
|
+
return self
|
|
391
|
+
|
|
392
|
+
def __exit__(self, *a):
|
|
393
|
+
self.close()
|
|
394
|
+
|
|
395
|
+
def __repr__(self):
|
|
396
|
+
return "WirexaClient(host=%r, cluster=%r)" % ("%s:%d" % self.address, self._cfg["cluster"])
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
MongoClient = WirexaClient # mismo nombre que pymongo: cambiar solo el import
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class Database:
|
|
403
|
+
def __init__(self, client, name):
|
|
404
|
+
self.client = client
|
|
405
|
+
self.name = name
|
|
406
|
+
|
|
407
|
+
def __getitem__(self, name):
|
|
408
|
+
return Collection(self, name)
|
|
409
|
+
|
|
410
|
+
def __getattr__(self, name):
|
|
411
|
+
if name.startswith("_"):
|
|
412
|
+
raise AttributeError(name)
|
|
413
|
+
return Collection(self, name)
|
|
414
|
+
|
|
415
|
+
def get_collection(self, name, **kwargs):
|
|
416
|
+
return Collection(self, name)
|
|
417
|
+
|
|
418
|
+
def list_collection_names(self, **kwargs):
|
|
419
|
+
return self.client._op("listCollections", True, db=self.name)["collections"]
|
|
420
|
+
|
|
421
|
+
def drop_collection(self, name):
|
|
422
|
+
if isinstance(name, Collection):
|
|
423
|
+
name = name.name
|
|
424
|
+
self.client._op("drop", db=self.name, coll=name)
|
|
425
|
+
|
|
426
|
+
def create_collection(self, name, **kwargs):
|
|
427
|
+
return Collection(self, name)
|
|
428
|
+
|
|
429
|
+
def command(self, cmd, *args, **kwargs):
|
|
430
|
+
if cmd == "ping" or (isinstance(cmd, dict) and "ping" in cmd):
|
|
431
|
+
self.client._op("ping", True)
|
|
432
|
+
return {"ok": 1.0}
|
|
433
|
+
raise OperationFailure("comando no soportado: %r" % (cmd,))
|
|
434
|
+
|
|
435
|
+
def __repr__(self):
|
|
436
|
+
return "Database(%r)" % self.name
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class Collection:
|
|
440
|
+
def __init__(self, database, name):
|
|
441
|
+
self.database = database
|
|
442
|
+
self.name = name
|
|
443
|
+
self._client = database.client
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def full_name(self):
|
|
447
|
+
return "%s.%s" % (self.database.name, self.name)
|
|
448
|
+
|
|
449
|
+
def __getattr__(self, name): # db.users.archived → colección "users.archived"
|
|
450
|
+
if name.startswith("_"):
|
|
451
|
+
raise AttributeError(name)
|
|
452
|
+
return Collection(self.database, "%s.%s" % (self.name, name))
|
|
453
|
+
|
|
454
|
+
def __getitem__(self, name):
|
|
455
|
+
return Collection(self.database, "%s.%s" % (self.name, name))
|
|
456
|
+
|
|
457
|
+
def _op(self, op, retry_safe=False, **f):
|
|
458
|
+
return self._client._op(op, retry_safe, db=self.database.name, coll=self.name, **f)
|
|
459
|
+
|
|
460
|
+
# -------- escritura
|
|
461
|
+
def insert_one(self, document, **kwargs):
|
|
462
|
+
r = self._op("insertOne", doc=document)
|
|
463
|
+
if "_id" not in document:
|
|
464
|
+
document["_id"] = r["insertedId"] # como pymongo: el dict queda con su _id
|
|
465
|
+
return InsertOneResult(r["insertedId"])
|
|
466
|
+
|
|
467
|
+
def insert_many(self, documents, ordered=True, **kwargs):
|
|
468
|
+
documents = list(documents)
|
|
469
|
+
if not documents:
|
|
470
|
+
raise TypeError("documents debe ser una lista no vacía")
|
|
471
|
+
r = self._op("insertMany", docs=documents, ordered=ordered)
|
|
472
|
+
for d, i in zip(documents, r["insertedIds"]):
|
|
473
|
+
d.setdefault("_id", i)
|
|
474
|
+
return InsertManyResult(r["insertedIds"])
|
|
475
|
+
|
|
476
|
+
def update_one(self, filter, update, upsert=False, **kwargs):
|
|
477
|
+
return UpdateResult(self._op("updateOne", filter=filter, update=update, upsert=upsert))
|
|
478
|
+
|
|
479
|
+
def update_many(self, filter, update, upsert=False, **kwargs):
|
|
480
|
+
return UpdateResult(self._op("updateMany", filter=filter, update=update, upsert=upsert))
|
|
481
|
+
|
|
482
|
+
def replace_one(self, filter, replacement, upsert=False, **kwargs):
|
|
483
|
+
return UpdateResult(self._op("replaceOne", filter=filter, replacement=replacement, upsert=upsert))
|
|
484
|
+
|
|
485
|
+
def delete_one(self, filter, **kwargs):
|
|
486
|
+
return DeleteResult(self._op("deleteOne", filter=filter))
|
|
487
|
+
|
|
488
|
+
def delete_many(self, filter, **kwargs):
|
|
489
|
+
return DeleteResult(self._op("deleteMany", filter=filter))
|
|
490
|
+
|
|
491
|
+
def find_one_and_update(self, filter, update, projection=None, sort=None, upsert=False,
|
|
492
|
+
return_document=ReturnDocument.BEFORE, **kwargs):
|
|
493
|
+
return self._op("findOneAndUpdate", filter=filter, update=update, projection=_projection(projection),
|
|
494
|
+
sort=_sort_spec(sort), upsert=upsert,
|
|
495
|
+
returnDocument="after" if return_document else "before")["doc"]
|
|
496
|
+
|
|
497
|
+
def find_one_and_replace(self, filter, replacement, projection=None, sort=None, upsert=False,
|
|
498
|
+
return_document=ReturnDocument.BEFORE, **kwargs):
|
|
499
|
+
return self._op("findOneAndReplace", filter=filter, replacement=replacement,
|
|
500
|
+
projection=_projection(projection), sort=_sort_spec(sort), upsert=upsert,
|
|
501
|
+
returnDocument="after" if return_document else "before")["doc"]
|
|
502
|
+
|
|
503
|
+
def find_one_and_delete(self, filter, projection=None, sort=None, **kwargs):
|
|
504
|
+
return self._op("findOneAndDelete", filter=filter, projection=_projection(projection),
|
|
505
|
+
sort=_sort_spec(sort))["doc"]
|
|
506
|
+
|
|
507
|
+
# -------- lectura
|
|
508
|
+
def find(self, filter=None, projection=None, skip=0, limit=0, sort=None, **kwargs):
|
|
509
|
+
return Cursor(self, filter or {}, _projection(projection), skip, limit, _sort_spec(sort))
|
|
510
|
+
|
|
511
|
+
def find_one(self, filter=None, projection=None, *args, sort=None, **kwargs):
|
|
512
|
+
if filter is not None and not isinstance(filter, dict):
|
|
513
|
+
filter = {"_id": filter}
|
|
514
|
+
return self._op("findOne", True, filter=filter or {}, projection=_projection(projection),
|
|
515
|
+
sort=_sort_spec(sort))["doc"]
|
|
516
|
+
|
|
517
|
+
def count_documents(self, filter, skip=0, limit=0, **kwargs):
|
|
518
|
+
return self._op("countDocuments", True, filter=filter, skip=skip, limit=limit)["n"]
|
|
519
|
+
|
|
520
|
+
def estimated_document_count(self, **kwargs):
|
|
521
|
+
return self._op("estimatedDocumentCount", True)["n"]
|
|
522
|
+
|
|
523
|
+
def distinct(self, key, filter=None, **kwargs):
|
|
524
|
+
return self._op("distinct", True, key=key, filter=filter or {})["values"]
|
|
525
|
+
|
|
526
|
+
def aggregate(self, pipeline, **kwargs):
|
|
527
|
+
return CommandCursor(self._op("aggregate", True, pipeline=list(pipeline))["docs"])
|
|
528
|
+
|
|
529
|
+
# -------- índices
|
|
530
|
+
def create_index(self, keys, unique=False, name=None, **kwargs):
|
|
531
|
+
f = {"keys": _sort_spec(keys), "unique": bool(unique)}
|
|
532
|
+
if name:
|
|
533
|
+
f["name"] = name
|
|
534
|
+
return self._op("createIndex", **f)["name"]
|
|
535
|
+
|
|
536
|
+
def create_indexes(self, indexes):
|
|
537
|
+
return [self.create_index(i.keys, **i.options) for i in indexes]
|
|
538
|
+
|
|
539
|
+
def drop_index(self, index_or_name):
|
|
540
|
+
name = index_or_name
|
|
541
|
+
if not isinstance(name, str):
|
|
542
|
+
spec = _sort_spec(index_or_name)
|
|
543
|
+
name = "_".join("%s_%s" % (k, d) for k, d in spec)
|
|
544
|
+
self._op("dropIndex", name=name)
|
|
545
|
+
|
|
546
|
+
def list_indexes(self):
|
|
547
|
+
out = []
|
|
548
|
+
for i in self._op("listIndexes", True)["indexes"]:
|
|
549
|
+
i["key"] = {k: d for k, d in i["key"]}
|
|
550
|
+
out.append(i)
|
|
551
|
+
return CommandCursor(out)
|
|
552
|
+
|
|
553
|
+
def index_information(self):
|
|
554
|
+
return {i["name"]: {"key": list(i["key"].items()), "unique": i.get("unique", False)}
|
|
555
|
+
for i in self.list_indexes()}
|
|
556
|
+
|
|
557
|
+
def drop(self):
|
|
558
|
+
self._op("drop")
|
|
559
|
+
|
|
560
|
+
def __repr__(self):
|
|
561
|
+
return "Collection(%r)" % self.full_name
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class IndexModel:
|
|
565
|
+
def __init__(self, keys, **kwargs):
|
|
566
|
+
self.keys = keys
|
|
567
|
+
self.options = kwargs
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class Cursor:
|
|
571
|
+
"""Cursor perezoso: la consulta sale al iterar (o con to_list)."""
|
|
572
|
+
|
|
573
|
+
def __init__(self, coll, filter, projection, skip, limit, sort):
|
|
574
|
+
self._coll = coll
|
|
575
|
+
self._filter = filter
|
|
576
|
+
self._projection = projection
|
|
577
|
+
self._skip = skip
|
|
578
|
+
self._limit = limit
|
|
579
|
+
self._sort = sort
|
|
580
|
+
self._docs = None
|
|
581
|
+
self._pos = 0
|
|
582
|
+
|
|
583
|
+
def _check(self):
|
|
584
|
+
if self._docs is not None:
|
|
585
|
+
raise WirexaDBError("no se puede modificar un cursor ya iterado")
|
|
586
|
+
|
|
587
|
+
def sort(self, key_or_list, direction=None):
|
|
588
|
+
self._check()
|
|
589
|
+
self._sort = _sort_spec(key_or_list, direction)
|
|
590
|
+
return self
|
|
591
|
+
|
|
592
|
+
def skip(self, n):
|
|
593
|
+
self._check()
|
|
594
|
+
self._skip = n
|
|
595
|
+
return self
|
|
596
|
+
|
|
597
|
+
def limit(self, n):
|
|
598
|
+
self._check()
|
|
599
|
+
self._limit = n
|
|
600
|
+
return self
|
|
601
|
+
|
|
602
|
+
def batch_size(self, n):
|
|
603
|
+
return self
|
|
604
|
+
|
|
605
|
+
def _fetch(self):
|
|
606
|
+
if self._docs is None:
|
|
607
|
+
self._docs = self._coll._op("find", True, filter=self._filter, projection=self._projection,
|
|
608
|
+
skip=self._skip, limit=self._limit, sort=self._sort)["docs"]
|
|
609
|
+
return self._docs
|
|
610
|
+
|
|
611
|
+
def __iter__(self):
|
|
612
|
+
return self
|
|
613
|
+
|
|
614
|
+
def __next__(self):
|
|
615
|
+
docs = self._fetch()
|
|
616
|
+
if self._pos >= len(docs):
|
|
617
|
+
raise StopIteration
|
|
618
|
+
d = docs[self._pos]
|
|
619
|
+
self._pos += 1
|
|
620
|
+
return d
|
|
621
|
+
|
|
622
|
+
next = __next__
|
|
623
|
+
|
|
624
|
+
def to_list(self, length=None):
|
|
625
|
+
docs = self._fetch()[self._pos:]
|
|
626
|
+
return docs if length is None else docs[:length]
|
|
627
|
+
|
|
628
|
+
def rewind(self):
|
|
629
|
+
self._docs = None
|
|
630
|
+
self._pos = 0
|
|
631
|
+
return self
|
|
632
|
+
|
|
633
|
+
def close(self):
|
|
634
|
+
pass
|
|
635
|
+
|
|
636
|
+
def __enter__(self):
|
|
637
|
+
return self
|
|
638
|
+
|
|
639
|
+
def __exit__(self, *a):
|
|
640
|
+
pass
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class CommandCursor:
|
|
644
|
+
def __init__(self, docs):
|
|
645
|
+
self._docs = docs
|
|
646
|
+
self._pos = 0
|
|
647
|
+
|
|
648
|
+
def __iter__(self):
|
|
649
|
+
return self
|
|
650
|
+
|
|
651
|
+
def __next__(self):
|
|
652
|
+
if self._pos >= len(self._docs):
|
|
653
|
+
raise StopIteration
|
|
654
|
+
d = self._docs[self._pos]
|
|
655
|
+
self._pos += 1
|
|
656
|
+
return d
|
|
657
|
+
|
|
658
|
+
next = __next__
|
|
659
|
+
|
|
660
|
+
def to_list(self, length=None):
|
|
661
|
+
docs = self._docs[self._pos:]
|
|
662
|
+
return docs if length is None else docs[:length]
|
|
663
|
+
|
|
664
|
+
def close(self):
|
|
665
|
+
pass
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wirexaapi
|
|
3
|
-
Version: 1.0
|
|
4
|
-
Summary: Wirexa API SDK - SignalWire/Twilio compatible
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Wirexa API SDK - voice API client (SignalWire/Twilio compatible) and Wirexa DB document database client
|
|
5
5
|
Author-email: Wirexa Team <dev@wirexa.io>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/wirexa/wirexa-api
|
|
@@ -84,3 +84,35 @@ The rest of your code stays the same!
|
|
|
84
84
|
## License
|
|
85
85
|
|
|
86
86
|
MIT
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## Wirexa DB (base de datos de documentos)
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from wirexaapi import MongoClient
|
|
93
|
+
|
|
94
|
+
client = MongoClient(
|
|
95
|
+
"wirexaapi+srv://usuario:password@cluster0-abc1234.wirexaapi.com/?appName=cluster0",
|
|
96
|
+
serverSelectionTimeoutMS=5000,
|
|
97
|
+
connectTimeoutMS=5000,
|
|
98
|
+
socketTimeoutMS=20000,
|
|
99
|
+
)
|
|
100
|
+
db = client["miapp"]
|
|
101
|
+
users = db["users"] # las colecciones se crean solas
|
|
102
|
+
|
|
103
|
+
users.insert_one({"name": "Ana", "age": 30})
|
|
104
|
+
users.find_one({"name": "Ana"})
|
|
105
|
+
for u in users.find({"age": {"$gt": 25}}).sort("name").limit(10):
|
|
106
|
+
print(u)
|
|
107
|
+
users.update_one({"name": "Ana"}, {"$set": {"age": 31}})
|
|
108
|
+
users.create_index("email", unique=True)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Soporta filtros (`$eq $ne $gt $gte $lt $lte $in $nin $exists $regex $and $or $nor $not $size $all $elemMatch`),
|
|
112
|
+
updates (`$set $unset $inc $mul $min $max $push $addToSet $pull $pop $rename $currentDate $setOnInsert`), upserts,
|
|
113
|
+
`find_one_and_*`, índices (incluidos únicos), `distinct`, `count_documents` y `aggregate`
|
|
114
|
+
(`$match $sort $skip $limit $project $addFields $unset $unwind $group $count $lookup`).
|
|
115
|
+
|
|
116
|
+
Si tu backend corre en la misma VPS que el cluster, usa la URI local que da el panel
|
|
117
|
+
(`wirexaapi://...@127.0.0.1:27501/?cluster=<id>&tls=false`): ~1 ms por consulta.
|
|
118
|
+
Instala `wirexaapi[srv]` para resolver el registro SRV del cluster.
|
wirexaapi-1.0.1/README.md
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# Wirexa API SDK
|
|
2
|
-
|
|
3
|
-
A production-ready Python SDK for Wirexa API - a self-hosted telephony platform compatible with SignalWire and Twilio REST API.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install wirexaapi
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
```python
|
|
14
|
-
from wirexaapi import Client
|
|
15
|
-
|
|
16
|
-
# Connect to your Wirexa API instance
|
|
17
|
-
client = Client(
|
|
18
|
-
project_id="your-project-id",
|
|
19
|
-
api_token="your-api-token",
|
|
20
|
-
signalwire_space_url="your-space.example.com:5000"
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
# Make a call
|
|
24
|
-
call = client.calls.create(
|
|
25
|
-
to="+573001234567",
|
|
26
|
-
from_="+18005550100",
|
|
27
|
-
url="https://your-backend.com/xml",
|
|
28
|
-
method="POST",
|
|
29
|
-
timeout=28,
|
|
30
|
-
record=True,
|
|
31
|
-
status_callback="https://your-backend.com/status",
|
|
32
|
-
status_callback_event=["initiated", "ringing", "completed"],
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
print(f"Call SID: {call.sid}")
|
|
36
|
-
print(f"Call Status: {call.status}")
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Migration from SignalWire
|
|
40
|
-
|
|
41
|
-
If you're migrating from SignalWire SDK:
|
|
42
|
-
|
|
43
|
-
```python
|
|
44
|
-
# Before
|
|
45
|
-
from signalwire.rest import Client as SignalWireClient
|
|
46
|
-
|
|
47
|
-
# After
|
|
48
|
-
from wirexaapi import Client as SignalWireClient
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
The rest of your code stays the same!
|
|
52
|
-
|
|
53
|
-
## Features
|
|
54
|
-
|
|
55
|
-
- Full SignalWire/Twilio API compatibility
|
|
56
|
-
- Call creation, retrieval, and listing
|
|
57
|
-
- Machine Detection (AMD)
|
|
58
|
-
- Recording
|
|
59
|
-
- Status callbacks
|
|
60
|
-
- Async AMD support
|
|
61
|
-
|
|
62
|
-
## License
|
|
63
|
-
|
|
64
|
-
MIT
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Wirexa API SDK - Drop-in replacement del SDK de SignalWire/Twilio.
|
|
3
|
-
|
|
4
|
-
CREDENCIALES (las obtienes del dashboard):
|
|
5
|
-
space_url = "your-space.example.com:5000"
|
|
6
|
-
project_id = "your-project-id"
|
|
7
|
-
api_token = "your-api-token"
|
|
8
|
-
|
|
9
|
-
USO:
|
|
10
|
-
|
|
11
|
-
ANTES (SignalWire original):
|
|
12
|
-
from signalwire.rest import Client
|
|
13
|
-
client = Client(project_id, api_token, signalwire_space_url=space_url)
|
|
14
|
-
|
|
15
|
-
DESPUÉS (Wirexa API):
|
|
16
|
-
from wirexaapi import Client
|
|
17
|
-
client = Client(project_id, api_token, signalwire_space_url=space_url)
|
|
18
|
-
|
|
19
|
-
El resto del código NO cambia.
|
|
20
|
-
"""
|
|
21
|
-
from wirexaapi.rest import Client
|
|
22
|
-
|
|
23
|
-
__all__ = ["Client"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|