api-service-handler 0.1.6__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.
- api_service_handler/__init__.py +77 -0
- api_service_handler/cli.py +341 -0
- api_service_handler/client.py +868 -0
- api_service_handler/config.py +177 -0
- api_service_handler/encryption.py +238 -0
- api_service_handler/enums.py +217 -0
- api_service_handler/exceptions.py +184 -0
- api_service_handler/models.py +301 -0
- api_service_handler/py.typed +0 -0
- api_service_handler/rate_limiter.py +187 -0
- api_service_handler/rotation.py +163 -0
- api_service_handler/storage/__init__.py +7 -0
- api_service_handler/storage/base.py +243 -0
- api_service_handler/storage/memory.py +229 -0
- api_service_handler/storage/mongodb.py +432 -0
- api_service_handler/storage/postgresql.py +429 -0
- api_service_handler/storage/sqlite.py +511 -0
- api_service_handler/usage_tracker.py +219 -0
- api_service_handler/utils.py +322 -0
- api_service_handler-0.1.6.dist-info/METADATA +282 -0
- api_service_handler-0.1.6.dist-info/RECORD +23 -0
- api_service_handler-0.1.6.dist-info/WHEEL +4 -0
- api_service_handler-0.1.6.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""MongoDB storage backend using motor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from datetime import date, datetime, timezone
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from ..enums import KeyStatus, Provider
|
|
10
|
+
from ..exceptions import DuplicateKeyError, KeyNotFoundError, StorageConnectionError
|
|
11
|
+
from ..models import APIKey, BulkOperationResult, KeyFilter, KeyUpdateRequest
|
|
12
|
+
from .base import StorageBackend
|
|
13
|
+
|
|
14
|
+
def _try_import_motor():
|
|
15
|
+
"""Lazy import motor with helpful error."""
|
|
16
|
+
try:
|
|
17
|
+
import motor.motor_asyncio
|
|
18
|
+
return motor.motor_asyncio
|
|
19
|
+
except ImportError:
|
|
20
|
+
raise ImportError(
|
|
21
|
+
"MongoDB backend requires 'motor'. "
|
|
22
|
+
"Install with: pip install api-service-handler[mongodb]"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
class MongoDBStorageBackend(StorageBackend):
|
|
26
|
+
"""MongoDB storage backend using motor.
|
|
27
|
+
|
|
28
|
+
Good for highly scalable, document-oriented storage.
|
|
29
|
+
Install with: pip install api-service-handler[mongodb]
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, connection_string: str, metadata_indexes: Optional[list[str]] = None) -> None:
|
|
33
|
+
"""Initialize MongoDB backend.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
connection_string: MongoDB connection URI, e.g., 'mongodb://localhost:27017/my_db'.
|
|
37
|
+
metadata_indexes: List of metadata keys to create compound indexes for.
|
|
38
|
+
"""
|
|
39
|
+
self._motor = _try_import_motor()
|
|
40
|
+
self._connection_string = connection_string
|
|
41
|
+
self._metadata_indexes = metadata_indexes or []
|
|
42
|
+
self._client = None
|
|
43
|
+
self._db = None
|
|
44
|
+
self._collection = None
|
|
45
|
+
self._initialized = False
|
|
46
|
+
|
|
47
|
+
async def initialize(self) -> None:
|
|
48
|
+
"""Connect to MongoDB and setup indexes."""
|
|
49
|
+
try:
|
|
50
|
+
self._client = self._motor.AsyncIOMotorClient(self._connection_string)
|
|
51
|
+
|
|
52
|
+
# Extract database name from URI, default to api_service_handler
|
|
53
|
+
db_name = self._client.get_database().name if self._client.get_database().name else "api_service_handler"
|
|
54
|
+
self._db = self._client[db_name]
|
|
55
|
+
self._collection = self._db["api_keys"]
|
|
56
|
+
|
|
57
|
+
# Create indexes
|
|
58
|
+
import pymongo
|
|
59
|
+
|
|
60
|
+
# Provider index
|
|
61
|
+
await self._collection.create_index([("provider", pymongo.ASCENDING)])
|
|
62
|
+
|
|
63
|
+
# Status index
|
|
64
|
+
await self._collection.create_index([("status", pymongo.ASCENDING)])
|
|
65
|
+
|
|
66
|
+
# Compound provider/status index
|
|
67
|
+
await self._collection.create_index(
|
|
68
|
+
[("provider", pymongo.ASCENDING), ("status", pymongo.ASCENDING)]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Dynamic compound indexes for metadata
|
|
72
|
+
for md_key in self._metadata_indexes:
|
|
73
|
+
await self._collection.create_index(
|
|
74
|
+
[
|
|
75
|
+
("provider", pymongo.ASCENDING),
|
|
76
|
+
("status", pymongo.ASCENDING),
|
|
77
|
+
(f"metadata.{md_key}", pymongo.ASCENDING)
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Unique compound index for active keys to prevent duplicates
|
|
82
|
+
try:
|
|
83
|
+
await self._collection.create_index(
|
|
84
|
+
[("key_value", pymongo.ASCENDING), ("provider", pymongo.ASCENDING)],
|
|
85
|
+
unique=True,
|
|
86
|
+
partialFilterExpression={"status": {"$ne": "revoked"}}
|
|
87
|
+
)
|
|
88
|
+
except pymongo.errors.OperationFailure:
|
|
89
|
+
# Fallback for AWS DocumentDB which does not support $ne in partial index
|
|
90
|
+
await self._collection.create_index(
|
|
91
|
+
[("key_value", pymongo.ASCENDING), ("provider", pymongo.ASCENDING)],
|
|
92
|
+
unique=True
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self._initialized = True
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise StorageConnectionError(backend="mongodb", detail=str(e))
|
|
98
|
+
|
|
99
|
+
async def close(self) -> None:
|
|
100
|
+
"""Close the MongoDB connection."""
|
|
101
|
+
if self._client:
|
|
102
|
+
self._client.close()
|
|
103
|
+
self._client = None
|
|
104
|
+
self._db = None
|
|
105
|
+
self._collection = None
|
|
106
|
+
self._initialized = False
|
|
107
|
+
|
|
108
|
+
def _dict_to_key(self, doc: dict[str, Any]) -> APIKey:
|
|
109
|
+
"""Convert a MongoDB document to an APIKey model."""
|
|
110
|
+
if not doc:
|
|
111
|
+
raise ValueError("Cannot convert empty document to APIKey")
|
|
112
|
+
|
|
113
|
+
# Map _id to id
|
|
114
|
+
if "_id" in doc:
|
|
115
|
+
doc["id"] = str(doc.pop("_id"))
|
|
116
|
+
|
|
117
|
+
return APIKey(**doc)
|
|
118
|
+
|
|
119
|
+
def _key_to_dict(self, key: APIKey) -> dict[str, Any]:
|
|
120
|
+
"""Convert an APIKey model to a MongoDB document."""
|
|
121
|
+
data = key.model_dump()
|
|
122
|
+
|
|
123
|
+
# Handle enums
|
|
124
|
+
data["provider"] = data["provider"].value if hasattr(data["provider"], "value") else data["provider"]
|
|
125
|
+
data["status"] = data["status"].value if hasattr(data["status"], "value") else data["status"]
|
|
126
|
+
data["environment"] = data["environment"].value if hasattr(data["environment"], "value") else data["environment"]
|
|
127
|
+
|
|
128
|
+
# MongoDB native datetime is UTC, but APIKey stores datetimes. We can dump to standard formats.
|
|
129
|
+
# Ensure datetimes are proper datetime objects for MongoDB
|
|
130
|
+
# pydantic model_dump handles basic types, but we want proper dates
|
|
131
|
+
# model_dump might convert dates to strings or leave as dates.
|
|
132
|
+
# Let's ensure proper MongoDB types
|
|
133
|
+
|
|
134
|
+
# map id to _id
|
|
135
|
+
data["_id"] = data.pop("id")
|
|
136
|
+
|
|
137
|
+
# Datetimes and dates are naturally handled by motor/pymongo if they are datetime objects,
|
|
138
|
+
# but pydantic might leave them. Let's ensure they are native types.
|
|
139
|
+
for field in ["last_used_at", "expires_at", "created_at", "updated_at"]:
|
|
140
|
+
if data.get(field) and isinstance(data[field], str):
|
|
141
|
+
data[field] = datetime.fromisoformat(data[field])
|
|
142
|
+
|
|
143
|
+
for field in ["last_reset_daily", "last_reset_monthly"]:
|
|
144
|
+
if data.get(field) and isinstance(data[field], str):
|
|
145
|
+
data[field] = datetime.fromisoformat(data[field]).date()
|
|
146
|
+
# pymongo requires datetime for dates too usually, convert date to datetime
|
|
147
|
+
if data.get(field) and isinstance(data[field], date) and not isinstance(data[field], datetime):
|
|
148
|
+
data[field] = datetime.combine(data[field], datetime.min.time(), tzinfo=timezone.utc)
|
|
149
|
+
|
|
150
|
+
return data
|
|
151
|
+
|
|
152
|
+
# ── CRUD ───────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
async def add_key(self, key: APIKey) -> APIKey:
|
|
155
|
+
doc = self._key_to_dict(key)
|
|
156
|
+
try:
|
|
157
|
+
await self._collection.insert_one(doc)
|
|
158
|
+
return key
|
|
159
|
+
except self._motor.pymongo.errors.DuplicateKeyError:
|
|
160
|
+
provider_val = key.provider.value if isinstance(key.provider, Provider) else key.provider
|
|
161
|
+
raise DuplicateKeyError(provider=provider_val, key_value=key.key_value)
|
|
162
|
+
|
|
163
|
+
async def get_key(self, key_id: str) -> APIKey:
|
|
164
|
+
doc = await self._collection.find_one({"_id": key_id})
|
|
165
|
+
if not doc:
|
|
166
|
+
raise KeyNotFoundError(key_id=key_id)
|
|
167
|
+
return self._dict_to_key(doc)
|
|
168
|
+
|
|
169
|
+
async def get_keys_by_provider(self, provider: Provider) -> list[APIKey]:
|
|
170
|
+
provider_val = provider.value if isinstance(provider, Provider) else provider
|
|
171
|
+
cursor = self._collection.find({"provider": provider_val})
|
|
172
|
+
return [self._dict_to_key(doc) async for doc in cursor]
|
|
173
|
+
|
|
174
|
+
async def get_all_keys(self, key_filter: Optional[KeyFilter] = None) -> list[APIKey]:
|
|
175
|
+
query = {}
|
|
176
|
+
|
|
177
|
+
if key_filter:
|
|
178
|
+
if key_filter.provider is not None:
|
|
179
|
+
provider_val = key_filter.provider.value if isinstance(key_filter.provider, Provider) else key_filter.provider
|
|
180
|
+
query["provider"] = provider_val
|
|
181
|
+
if key_filter.status is not None:
|
|
182
|
+
status_val = key_filter.status.value if isinstance(key_filter.status, KeyStatus) else key_filter.status
|
|
183
|
+
query["status"] = status_val
|
|
184
|
+
if key_filter.environment is not None:
|
|
185
|
+
env_val = key_filter.environment.value if hasattr(key_filter.environment, "value") else key_filter.environment
|
|
186
|
+
query["environment"] = env_val
|
|
187
|
+
if key_filter.alias_contains:
|
|
188
|
+
query["alias"] = {"$regex": key_filter.alias_contains, "$options": "i"}
|
|
189
|
+
if key_filter.tags:
|
|
190
|
+
query["tags"] = {"$in": key_filter.tags}
|
|
191
|
+
if key_filter.metadata_filter:
|
|
192
|
+
for k, v in key_filter.metadata_filter.items():
|
|
193
|
+
query[f"metadata.{k}"] = v
|
|
194
|
+
|
|
195
|
+
# Sorting logic: priority asc, created_at asc
|
|
196
|
+
cursor = self._collection.find(query).sort([("priority", 1), ("created_at", 1)])
|
|
197
|
+
keys = [self._dict_to_key(doc) async for doc in cursor]
|
|
198
|
+
|
|
199
|
+
# Capacity filter applies in memory
|
|
200
|
+
if key_filter and key_filter.has_capacity is not None:
|
|
201
|
+
keys = [k for k in keys if k.has_capacity == key_filter.has_capacity]
|
|
202
|
+
|
|
203
|
+
return keys
|
|
204
|
+
|
|
205
|
+
async def update_key(self, key_id: str, updates: KeyUpdateRequest) -> APIKey:
|
|
206
|
+
# Check exists
|
|
207
|
+
await self.get_key(key_id)
|
|
208
|
+
|
|
209
|
+
update_data = updates.model_dump(exclude_unset=True)
|
|
210
|
+
if not update_data:
|
|
211
|
+
return await self.get_key(key_id)
|
|
212
|
+
|
|
213
|
+
# Handle enums
|
|
214
|
+
if "status" in update_data:
|
|
215
|
+
val = update_data["status"]
|
|
216
|
+
update_data["status"] = val.value if hasattr(val, "value") else val
|
|
217
|
+
if "environment" in update_data:
|
|
218
|
+
val = update_data["environment"]
|
|
219
|
+
update_data["environment"] = val.value if hasattr(val, "value") else val
|
|
220
|
+
|
|
221
|
+
# Handle datetimes
|
|
222
|
+
if "expires_at" in update_data and update_data["expires_at"]:
|
|
223
|
+
if isinstance(update_data["expires_at"], str):
|
|
224
|
+
update_data["expires_at"] = datetime.fromisoformat(update_data["expires_at"])
|
|
225
|
+
|
|
226
|
+
update_data["updated_at"] = datetime.now(timezone.utc)
|
|
227
|
+
|
|
228
|
+
await self._collection.update_one({"_id": key_id}, {"$set": update_data})
|
|
229
|
+
return await self.get_key(key_id)
|
|
230
|
+
|
|
231
|
+
async def delete_key(self, key_id: str, soft: bool = True) -> bool:
|
|
232
|
+
await self.get_key(key_id)
|
|
233
|
+
|
|
234
|
+
if soft:
|
|
235
|
+
now = datetime.now(timezone.utc)
|
|
236
|
+
await self._collection.update_one(
|
|
237
|
+
{"_id": key_id},
|
|
238
|
+
{"$set": {"status": "revoked", "updated_at": now}}
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
await self._collection.delete_one({"_id": key_id})
|
|
242
|
+
|
|
243
|
+
return True
|
|
244
|
+
|
|
245
|
+
# ── Usage Tracking ─────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
async def increment_usage(
|
|
248
|
+
self,
|
|
249
|
+
key_id: str,
|
|
250
|
+
daily: int = 1,
|
|
251
|
+
monthly: int = 1,
|
|
252
|
+
total: int = 1,
|
|
253
|
+
) -> None:
|
|
254
|
+
now = datetime.now(timezone.utc)
|
|
255
|
+
await self._collection.update_one(
|
|
256
|
+
{"_id": key_id},
|
|
257
|
+
{
|
|
258
|
+
"$inc": {
|
|
259
|
+
"daily_usage_count": daily,
|
|
260
|
+
"monthly_usage_count": monthly,
|
|
261
|
+
"total_usage_count": total
|
|
262
|
+
},
|
|
263
|
+
"$set": {
|
|
264
|
+
"last_used_at": now,
|
|
265
|
+
"updated_at": now
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
async def update_concurrent_usage(self, key_id: str, delta: int) -> int:
|
|
271
|
+
now = datetime.now(timezone.utc)
|
|
272
|
+
import pymongo
|
|
273
|
+
# Aggregation pipeline update (MongoDB 4.2+) — atomically clamps to 0.
|
|
274
|
+
updated = await self._collection.find_one_and_update(
|
|
275
|
+
{"_id": key_id},
|
|
276
|
+
[
|
|
277
|
+
{
|
|
278
|
+
"$set": {
|
|
279
|
+
"concurrent_usage": {
|
|
280
|
+
"$max": [0, {"$add": ["$concurrent_usage", delta]}]
|
|
281
|
+
},
|
|
282
|
+
"updated_at": now,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
return_document=pymongo.ReturnDocument.AFTER,
|
|
287
|
+
)
|
|
288
|
+
if not updated:
|
|
289
|
+
raise KeyNotFoundError(key_id=key_id)
|
|
290
|
+
return updated.get("concurrent_usage", 0)
|
|
291
|
+
|
|
292
|
+
async def reset_daily_counts(self, before_date: date) -> int:
|
|
293
|
+
now = datetime.now(timezone.utc)
|
|
294
|
+
before_dt = datetime.combine(before_date, datetime.min.time(), tzinfo=timezone.utc)
|
|
295
|
+
|
|
296
|
+
result = await self._collection.update_many(
|
|
297
|
+
{"last_reset_daily": {"$lt": before_dt}},
|
|
298
|
+
{
|
|
299
|
+
"$set": {
|
|
300
|
+
"daily_usage_count": 0,
|
|
301
|
+
"last_reset_daily": before_dt,
|
|
302
|
+
"updated_at": now
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Recover rate-limited keys if monthly is ok
|
|
308
|
+
# For simplicity, we fetch rate_limited keys and check them
|
|
309
|
+
# It's harder to do a complex conditionally-update-many in Mongo based on other fields
|
|
310
|
+
cursor = self._collection.find({"status": "rate_limited"})
|
|
311
|
+
async for doc in cursor:
|
|
312
|
+
monthly_limit = doc.get("monthly_limit")
|
|
313
|
+
monthly_count = doc.get("monthly_usage_count", 0)
|
|
314
|
+
if monthly_limit is None or monthly_count < monthly_limit:
|
|
315
|
+
# also check daily, but we just reset it
|
|
316
|
+
daily_limit = doc.get("daily_limit")
|
|
317
|
+
daily_count = doc.get("daily_usage_count", 0)
|
|
318
|
+
if daily_limit is None or daily_count < daily_limit:
|
|
319
|
+
await self._collection.update_one(
|
|
320
|
+
{"_id": doc["_id"]},
|
|
321
|
+
{"$set": {"status": "active", "updated_at": now}}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return result.modified_count
|
|
325
|
+
|
|
326
|
+
async def reset_monthly_counts(self, before_date: date) -> int:
|
|
327
|
+
now = datetime.now(timezone.utc)
|
|
328
|
+
before_dt = datetime.combine(before_date, datetime.min.time(), tzinfo=timezone.utc)
|
|
329
|
+
|
|
330
|
+
# To reset correctly, we need to find keys where month or year differs
|
|
331
|
+
# MongoDB aggregation would be complex, let's just fetch last_reset_monthly
|
|
332
|
+
cursor = self._collection.find({}, {"_id": 1, "last_reset_monthly": 1, "status": 1})
|
|
333
|
+
ids_to_reset = []
|
|
334
|
+
async for doc in cursor:
|
|
335
|
+
last_reset = doc.get("last_reset_monthly")
|
|
336
|
+
if last_reset:
|
|
337
|
+
if isinstance(last_reset, datetime):
|
|
338
|
+
last_reset_date = last_reset.date()
|
|
339
|
+
else:
|
|
340
|
+
last_reset_date = date.fromisoformat(last_reset) if isinstance(last_reset, str) else last_reset
|
|
341
|
+
|
|
342
|
+
if last_reset_date.month != before_date.month or last_reset_date.year != before_date.year:
|
|
343
|
+
ids_to_reset.append(doc["_id"])
|
|
344
|
+
|
|
345
|
+
if not ids_to_reset:
|
|
346
|
+
return 0
|
|
347
|
+
|
|
348
|
+
await self._collection.update_many(
|
|
349
|
+
{"_id": {"$in": ids_to_reset}},
|
|
350
|
+
{
|
|
351
|
+
"$set": {
|
|
352
|
+
"monthly_usage_count": 0,
|
|
353
|
+
"last_reset_monthly": before_dt,
|
|
354
|
+
"updated_at": now
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Recover rate-limited
|
|
360
|
+
await self._collection.update_many(
|
|
361
|
+
{"_id": {"$in": ids_to_reset}, "status": "rate_limited"},
|
|
362
|
+
{"$set": {"status": "active", "updated_at": now}}
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return len(ids_to_reset)
|
|
366
|
+
|
|
367
|
+
async def update_last_used(self, key_id: str) -> None:
|
|
368
|
+
now = datetime.now(timezone.utc)
|
|
369
|
+
await self._collection.update_one(
|
|
370
|
+
{"_id": key_id},
|
|
371
|
+
{"$set": {"last_used_at": now, "updated_at": now}}
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# ── Bulk Operations ────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
async def bulk_add_keys(self, keys: list[APIKey]) -> BulkOperationResult:
|
|
377
|
+
result = BulkOperationResult(total=len(keys))
|
|
378
|
+
|
|
379
|
+
if not keys:
|
|
380
|
+
return result
|
|
381
|
+
|
|
382
|
+
# For bulk add, doing one by one to catch individual errors and not fail all
|
|
383
|
+
for key in keys:
|
|
384
|
+
try:
|
|
385
|
+
await self.add_key(key)
|
|
386
|
+
result.successful += 1
|
|
387
|
+
result.created_ids.append(key.id)
|
|
388
|
+
except Exception as e:
|
|
389
|
+
result.failed += 1
|
|
390
|
+
result.errors.append(f"Key {key.alias or key.id}: {e}")
|
|
391
|
+
|
|
392
|
+
return result
|
|
393
|
+
|
|
394
|
+
async def bulk_delete_keys(self, key_ids: list[str], soft: bool = True) -> BulkOperationResult:
|
|
395
|
+
result = BulkOperationResult(total=len(key_ids))
|
|
396
|
+
|
|
397
|
+
if not key_ids:
|
|
398
|
+
return result
|
|
399
|
+
|
|
400
|
+
for key_id in key_ids:
|
|
401
|
+
try:
|
|
402
|
+
await self.delete_key(key_id, soft=soft)
|
|
403
|
+
result.successful += 1
|
|
404
|
+
except Exception as e:
|
|
405
|
+
result.failed += 1
|
|
406
|
+
result.errors.append(f"Key {key_id}: {e}")
|
|
407
|
+
|
|
408
|
+
return result
|
|
409
|
+
|
|
410
|
+
# ── Health ─────────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
async def health_check(self) -> bool:
|
|
413
|
+
if not self._client or not self._initialized:
|
|
414
|
+
return False
|
|
415
|
+
try:
|
|
416
|
+
await self._client.admin.command('ping')
|
|
417
|
+
return True
|
|
418
|
+
except Exception:
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
async def count_keys(
|
|
422
|
+
self,
|
|
423
|
+
provider: Optional[Provider] = None,
|
|
424
|
+
status: Optional[KeyStatus] = None,
|
|
425
|
+
) -> int:
|
|
426
|
+
query = {}
|
|
427
|
+
if provider is not None:
|
|
428
|
+
query["provider"] = provider.value if hasattr(provider, "value") else provider
|
|
429
|
+
if status is not None:
|
|
430
|
+
query["status"] = status.value if hasattr(status, "value") else status
|
|
431
|
+
|
|
432
|
+
return await self._collection.count_documents(query)
|