Skip to content

Commit 3b6f6f3

Browse files
authored
Added doctest to files in mcpgateway folder (#445)
Signed-off-by: Manav Gupta <[email protected]>
1 parent 031342e commit 3b6f6f3

File tree

4 files changed

+143
-13
lines changed

4 files changed

+143
-13
lines changed

mcpgateway/config.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
import logging
5454
from pathlib import Path
5555
import re
56-
from typing import Annotated, Any, Dict, List, Optional, Set, Union, ClassVar
56+
from typing import Annotated, Any, ClassVar, Dict, List, Optional, Set, Union
5757

5858
# Third-Party
5959
from fastapi import HTTPException
@@ -410,7 +410,13 @@ def validate_transport(self) -> None:
410410
raise ValueError(f"Invalid transport type. Must be one of: {valid_types}")
411411

412412
def validate_database(self) -> None:
413-
"""Validate database configuration."""
413+
"""Validate database configuration.
414+
415+
Examples:
416+
>>> from mcpgateway.config import Settings
417+
>>> s = Settings(database_url='sqlite:///./test.db')
418+
>>> s.validate_database() # Should create the directory if it does not exist
419+
"""
414420
if self.database_url.startswith("sqlite"):
415421
db_path = Path(self.database_url.replace("sqlite:///", ""))
416422
db_dir = db_path.parent
@@ -469,6 +475,18 @@ def extract_using_jq(data, jq_filter=""):
469475
470476
Returns:
471477
The result of applying the jq filter to the input data.
478+
479+
Examples:
480+
>>> extract_using_jq('{"a": 1, "b": 2}', '.a')
481+
[1]
482+
>>> extract_using_jq({'a': 1, 'b': 2}, '.b')
483+
[2]
484+
>>> extract_using_jq('[{"a": 1}, {"a": 2}]', '.[].a')
485+
[1, 2]
486+
>>> extract_using_jq('not a json', '.a')
487+
['Invalid JSON string provided.']
488+
>>> extract_using_jq({'a': 1}, '')
489+
{'a': 1}
472490
"""
473491
if jq_filter == "":
474492
return data
@@ -513,6 +531,16 @@ def jsonpath_modifier(data: Any, jsonpath: str = "$[*]", mappings: Optional[Dict
513531
514532
Raises:
515533
HTTPException: If there's an error parsing or executing the JSONPath expressions.
534+
535+
Examples:
536+
>>> jsonpath_modifier({'a': 1, 'b': 2}, '$.a')
537+
[1]
538+
>>> jsonpath_modifier([{'a': 1}, {'a': 2}], '$[*].a')
539+
[1, 2]
540+
>>> jsonpath_modifier({'a': {'b': 2}}, '$.a.b')
541+
[2]
542+
>>> jsonpath_modifier({'a': 1}, '$.b')
543+
[]
516544
"""
517545
if not jsonpath:
518546
jsonpath = "$[*]"

mcpgateway/db.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
from mcpgateway.utils.db_isready import wait_for_db_ready
6464
from mcpgateway.validators import SecurityValidator
6565

66-
6766
# ---------------------------------------------------------------------------
6867
# 1. Parse the URL so we can inspect backend ("postgresql", "sqlite", ...)
6968
# and the specific driver ("psycopg2", "asyncpg", empty string = default).
@@ -121,6 +120,12 @@ def utc_now() -> datetime:
121120
Returns:
122121
datetime: A timezone-aware `datetime` whose `tzinfo` is
123122
`datetime.timezone.utc`.
123+
124+
Examples:
125+
>>> from mcpgateway.db import utc_now
126+
>>> now = utc_now()
127+
>>> now.tzinfo is not None
128+
True
124129
"""
125130
return datetime.now(timezone.utc)
126131

mcpgateway/schemas.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,13 @@ def to_camel_case(s: str) -> str:
5353
Returns:
5454
str: The string converted to camelCase.
5555
56-
Example:
56+
Examples:
5757
>>> to_camel_case("hello_world_example")
5858
'helloWorldExample'
59+
>>> to_camel_case("alreadyCamel")
60+
'alreadyCamel'
61+
>>> to_camel_case("")
62+
''
5963
"""
6064
return "".join(word.capitalize() if i else word for i, word in enumerate(s.split("_")))
6165

@@ -70,7 +74,8 @@ def encode_datetime(v: datetime) -> str:
7074
Returns:
7175
str: The ISO 8601 formatted string representation of the datetime object.
7276
73-
Example:
77+
Examples:
78+
>>> from datetime import datetime
7479
>>> encode_datetime(datetime(2023, 5, 22, 14, 30, 0))
7580
'2023-05-22T14:30:00'
7681
"""
@@ -107,6 +112,14 @@ def to_dict(self, use_alias: bool = False) -> Dict[str, Any]:
107112
Returns:
108113
Dict[str, Any]: A dictionary where keys are field names and values are corresponding field values,
109114
with any nested models recursively converted to dictionaries.
115+
116+
Examples:
117+
>>> class ExampleModel(BaseModelWithConfigDict):
118+
... foo: int
119+
... bar: str
120+
>>> m = ExampleModel(foo=1, bar='baz')
121+
>>> m.to_dict()
122+
{'foo': 1, 'bar': 'baz'}
110123
"""
111124
output = {}
112125
for key, value in self.dict(by_alias=use_alias).items():
@@ -294,6 +307,15 @@ def validate_name(cls, v: str) -> str:
294307
295308
Returns:
296309
str: Value if validated as safe
310+
311+
Examples:
312+
>>> from mcpgateway.schemas import ToolCreate
313+
>>> ToolCreate.validate_name('valid_tool')
314+
'valid_tool'
315+
>>> ToolCreate.validate_name('Invalid Tool!')
316+
Traceback (most recent call last):
317+
...
318+
ValueError: ...
297319
"""
298320
return SecurityValidator.validate_tool_name(v)
299321

@@ -307,6 +329,15 @@ def validate_url(cls, v: str) -> str:
307329
308330
Returns:
309331
str: Value if validated as safe
332+
333+
Examples:
334+
>>> from mcpgateway.schemas import ToolCreate
335+
>>> ToolCreate.validate_url('https://example.com')
336+
'https://example.com'
337+
>>> ToolCreate.validate_url('ftp://example.com')
338+
Traceback (most recent call last):
339+
...
340+
ValueError: ...
310341
"""
311342
return SecurityValidator.validate_url(v, "Tool URL")
312343

@@ -323,6 +354,15 @@ def validate_description(cls, v: Optional[str]) -> Optional[str]:
323354
324355
Raises:
325356
ValueError: When value is unsafe
357+
358+
Examples:
359+
>>> from mcpgateway.schemas import ToolCreate
360+
>>> ToolCreate.validate_description('A safe description')
361+
'A safe description'
362+
>>> ToolCreate.validate_description('x' * 5000)
363+
Traceback (most recent call last):
364+
...
365+
ValueError: ...
326366
"""
327367
if v is None:
328368
return v
@@ -340,6 +380,15 @@ def validate_json_fields(cls, v: Dict[str, Any]) -> Dict[str, Any]:
340380
341381
Returns:
342382
dict: Value if validated as safe
383+
384+
Examples:
385+
>>> from mcpgateway.schemas import ToolCreate
386+
>>> ToolCreate.validate_json_fields({'a': 1})
387+
{'a': 1}
388+
>>> ToolCreate.validate_json_fields({'a': {'b': {'c': {'d': {'e': {'f': {'g': {'h': {'i': {'j': {'k': 1}}}}}}}}}}})
389+
Traceback (most recent call last):
390+
...
391+
ValueError: ...
343392
"""
344393
SecurityValidator.validate_json_depth(v)
345394
return v

mcpgateway/validators.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ def validate_name(cls, value: str, field_name: str = "Name") -> str:
112112
113113
Raises:
114114
ValueError: When input is not acceptable
115+
116+
Examples:
117+
>>> SecurityValidator.validate_name('valid_name')
118+
'valid_name'
119+
>>> SecurityValidator.validate_name('Invalid Name!')
120+
Traceback (most recent call last):
121+
...
122+
ValueError: ...
115123
"""
116124
if not value:
117125
raise ValueError(f"{field_name} cannot be empty")
@@ -142,6 +150,14 @@ def validate_identifier(cls, value: str, field_name: str) -> str:
142150
143151
Raises:
144152
ValueError: When input is not acceptable
153+
154+
Examples:
155+
>>> SecurityValidator.validate_identifier('valid_id', 'ID')
156+
'valid_id'
157+
>>> SecurityValidator.validate_identifier('Invalid/ID', 'ID')
158+
Traceback (most recent call last):
159+
...
160+
ValueError: ...
145161
"""
146162
if not value:
147163
raise ValueError(f"{field_name} cannot be empty")
@@ -172,6 +188,14 @@ def validate_uri(cls, value: str, field_name: str = "URI") -> str:
172188
173189
Raises:
174190
ValueError: When input is not acceptable
191+
192+
Examples:
193+
>>> SecurityValidator.validate_uri('/valid/uri', 'URI')
194+
'/valid/uri'
195+
>>> SecurityValidator.validate_uri('..', 'URI')
196+
Traceback (most recent call last):
197+
...
198+
ValueError: ...
175199
"""
176200
if not value:
177201
raise ValueError(f"{field_name} cannot be empty")
@@ -203,6 +227,14 @@ def validate_tool_name(cls, value: str) -> str:
203227
204228
Raises:
205229
ValueError: When input is not acceptable
230+
231+
Examples:
232+
>>> SecurityValidator.validate_tool_name('tool_1')
233+
'tool_1'
234+
>>> SecurityValidator.validate_tool_name('1tool')
235+
Traceback (most recent call last):
236+
...
237+
ValueError: ...
206238
"""
207239
if not value:
208240
raise ValueError("Tool name cannot be empty")
@@ -252,17 +284,25 @@ def validate_template(cls, value: str) -> str:
252284

253285
@classmethod
254286
def validate_url(cls, value: str, field_name: str = "URL") -> str:
255-
"""Validate URL format and ensure safe display
287+
"""Validate URLs for allowed schemes and safe display
256288
257289
Args:
258290
value (str): Value to validate
259-
field_name (str): Name of the field being validated
291+
field_name (str): Name of field being validated
260292
261293
Returns:
262294
str: Value if acceptable
263295
264296
Raises:
265297
ValueError: When input is not acceptable
298+
299+
Examples:
300+
>>> SecurityValidator.validate_url('https://example.com')
301+
'https://example.com'
302+
>>> SecurityValidator.validate_url('ftp://example.com')
303+
Traceback (most recent call last):
304+
...
305+
ValueError: ...
266306
"""
267307
if not value:
268308
raise ValueError(f"{field_name} cannot be empty")
@@ -294,17 +334,25 @@ def validate_url(cls, value: str, field_name: str = "URL") -> str:
294334

295335
@classmethod
296336
def validate_json_depth(cls, obj: Any, max_depth: int = None, current_depth: int = 0) -> None:
297-
"""Check JSON structure doesn't exceed maximum depth
337+
"""Validate the maximum depth of a JSON object
298338
299339
Args:
300-
obj (Any): JSON object to validate
301-
max_depth (int): Max allowed depth for a JSON
302-
current_depth (int): Depth of nested structure in JSON
340+
obj (Any): The JSON object to check
341+
max_depth (int, optional): Maximum allowed depth. Defaults to class setting.
342+
current_depth (int): Used for recursion, do not set manually.
303343
304344
Raises:
305-
ValueError: When input is not acceptable
345+
ValueError: If the object exceeds the maximum allowed depth
346+
347+
Examples:
348+
>>> SecurityValidator.validate_json_depth({'a': {'b': {'c': 1}}}, max_depth=3)
349+
>>> SecurityValidator.validate_json_depth({'a': {'b': {'c': {'d': 1}}}}, max_depth=3)
350+
Traceback (most recent call last):
351+
...
352+
ValueError: ...
306353
"""
307-
max_depth = max_depth or cls.MAX_JSON_DEPTH
354+
if max_depth is None:
355+
max_depth = cls.MAX_JSON_DEPTH
308356

309357
if current_depth > max_depth:
310358
raise ValueError(f"JSON structure exceeds maximum depth of {max_depth}")

0 commit comments

Comments
 (0)