dify_plugin 0.2.0__tar.gz → 0.2.2__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.
Files changed (128) hide show
  1. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/PKG-INFO +2 -2
  2. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/README.md +1 -1
  3. dify_plugin-0.2.2/dify_plugin/cli.py +16 -0
  4. dify_plugin-0.2.2/dify_plugin/commands/__init__.py +3 -0
  5. dify_plugin-0.2.2/dify_plugin/commands/generate_docs.py +5 -0
  6. dify_plugin-0.2.2/dify_plugin/core/documentation/generator.py +416 -0
  7. dify_plugin-0.2.2/dify_plugin/core/documentation/schema_doc.py +78 -0
  8. dify_plugin-0.2.2/dify_plugin/core/entities/plugin/setup.py +169 -0
  9. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/plugin_executor.py +7 -1
  10. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/stdio/request_reader.py +14 -6
  11. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/tcp/request_reader.py +1 -2
  12. dify_plugin-0.2.2/dify_plugin/core/utils/http_parser.py +54 -0
  13. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/__init__.py +5 -0
  14. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/agent.py +47 -3
  15. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/endpoint.py +27 -2
  16. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/__init__.py +57 -26
  17. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/provider.py +64 -6
  18. dify_plugin-0.2.2/dify_plugin/entities/oauth.py +17 -0
  19. dify_plugin-0.2.2/dify_plugin/entities/provider_config.py +116 -0
  20. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/tool.py +83 -67
  21. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/file/file.py +14 -3
  22. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/agent/__init__.py +3 -1
  23. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/llm.py +56 -42
  24. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/tool/__init__.py +17 -1
  25. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/pyproject.toml +1 -1
  26. dify_plugin-0.2.2/tests/__init__.py +0 -0
  27. dify_plugin-0.2.2/tests/entities/endpoint/test_endpoint_group.py +53 -0
  28. dify_plugin-0.2.2/tests/entities/plugin/test_declaration.py +24 -0
  29. dify_plugin-0.2.2/tests/interfaces/model/__init__.py +0 -0
  30. dify_plugin-0.2.2/tests/interfaces/model/openai_compatible/__init__.py +0 -0
  31. dify_plugin-0.2.2/tests/interfaces/model/openai_compatible/test_increase_tool_call.py +99 -0
  32. dify_plugin-0.2.2/tests/servers/test_stdio.py +76 -0
  33. dify_plugin-0.2.2/tests/utils/test_http_parser.py +54 -0
  34. dify_plugin-0.2.0/dify_plugin/core/entities/plugin/setup.py +0 -101
  35. dify_plugin-0.2.0/dify_plugin/core/utils/http_parser.py +0 -14
  36. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/LICENSE +0 -0
  37. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/__init__.py +0 -0
  38. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/config/__init__.py +0 -0
  39. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/config/config.py +0 -0
  40. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/config/logger_format.py +0 -0
  41. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/__init__.py +0 -0
  42. {dify_plugin-0.2.0/dify_plugin/core/entities → dify_plugin-0.2.2/dify_plugin/core/documentation}/__init__.py +0 -0
  43. {dify_plugin-0.2.0/dify_plugin/core/entities/plugin → dify_plugin-0.2.2/dify_plugin/core/entities}/__init__.py +0 -0
  44. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/invocation.py +0 -0
  45. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/message.py +0 -0
  46. {dify_plugin-0.2.0/dify_plugin/core/server/__base → dify_plugin-0.2.2/dify_plugin/core/entities/plugin}/__init__.py +0 -0
  47. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/plugin/io.py +0 -0
  48. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/plugin/parameter_type.py +0 -0
  49. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/entities/plugin/request.py +0 -0
  50. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/plugin_registration.py +0 -0
  51. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/runtime.py +0 -0
  52. {dify_plugin-0.2.0/dify_plugin/core/server → dify_plugin-0.2.2/dify_plugin/core/server/__base}/__init__.py +0 -0
  53. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/__base/filter_reader.py +0 -0
  54. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/__base/request_reader.py +0 -0
  55. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/__base/response_writer.py +0 -0
  56. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/__base/writer_entities.py +0 -0
  57. {dify_plugin-0.2.0/dify_plugin/core/server/serverless → dify_plugin-0.2.2/dify_plugin/core/server}/__init__.py +0 -0
  58. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/io_server.py +0 -0
  59. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/router.py +0 -0
  60. {dify_plugin-0.2.0/dify_plugin/core/server/stdio → dify_plugin-0.2.2/dify_plugin/core/server/serverless}/__init__.py +0 -0
  61. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/serverless/request_reader.py +0 -0
  62. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/serverless/response_writer.py +0 -0
  63. {dify_plugin-0.2.0/dify_plugin/core/server/tcp → dify_plugin-0.2.2/dify_plugin/core/server/stdio}/__init__.py +0 -0
  64. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/server/stdio/response_writer.py +0 -0
  65. {dify_plugin-0.2.0/dify_plugin/core/utils → dify_plugin-0.2.2/dify_plugin/core/server/tcp}/__init__.py +0 -0
  66. {dify_plugin-0.2.0/dify_plugin/errors → dify_plugin-0.2.2/dify_plugin/core/utils}/__init__.py +0 -0
  67. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/utils/class_loader.py +0 -0
  68. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/utils/position_helper.py +0 -0
  69. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/core/utils/yaml_loader.py +0 -0
  70. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/llm.py +0 -0
  71. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/message.py +0 -0
  72. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/moderation.py +0 -0
  73. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/rerank.py +0 -0
  74. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/speech2text.py +0 -0
  75. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/text_embedding.py +0 -0
  76. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/model/tts.py +0 -0
  77. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/entities/workflow_node.py +0 -0
  78. {dify_plugin-0.2.0/dify_plugin/file → dify_plugin-0.2.2/dify_plugin/errors}/__init__.py +0 -0
  79. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/errors/model.py +0 -0
  80. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/errors/tool.py +0 -0
  81. {dify_plugin-0.2.0/dify_plugin/interfaces → dify_plugin-0.2.2/dify_plugin/file}/__init__.py +0 -0
  82. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/file/constants.py +0 -0
  83. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/file/entities.py +0 -0
  84. {dify_plugin-0.2.0/dify_plugin/interfaces/model/openai_compatible → dify_plugin-0.2.2/dify_plugin/interfaces}/__init__.py +0 -0
  85. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/endpoint/__init__.py +0 -0
  86. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/__init__.py +0 -0
  87. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/ai_model.py +0 -0
  88. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/audio.mp3 +0 -0
  89. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/large_language_model.py +0 -0
  90. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/moderation_model.py +0 -0
  91. {dify_plugin-0.2.0/dify_plugin/invocations → dify_plugin-0.2.2/dify_plugin/interfaces/model/openai_compatible}/__init__.py +0 -0
  92. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/common.py +0 -0
  93. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/provider.py +0 -0
  94. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/rerank.py +0 -0
  95. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/speech2text.py +0 -0
  96. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/text_embedding.py +0 -0
  97. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/openai_compatible/tts.py +0 -0
  98. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/rerank_model.py +0 -0
  99. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/speech2text_model.py +0 -0
  100. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/text_embedding_model.py +0 -0
  101. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/interfaces/model/tts_model.py +0 -0
  102. {dify_plugin-0.2.0/dify_plugin/invocations/model → dify_plugin-0.2.2/dify_plugin/invocations}/__init__.py +0 -0
  103. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/app/__init__.py +0 -0
  104. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/app/chat.py +0 -0
  105. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/app/completion.py +0 -0
  106. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/app/workflow.py +0 -0
  107. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/file.py +0 -0
  108. {dify_plugin-0.2.0/dify_plugin/invocations/workflow_node → dify_plugin-0.2.2/dify_plugin/invocations/model}/__init__.py +0 -0
  109. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/llm.py +0 -0
  110. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/moderation.py +0 -0
  111. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/rerank.py +0 -0
  112. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/speech2text.py +0 -0
  113. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/text_embedding.py +0 -0
  114. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/model/tts.py +0 -0
  115. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/storage.py +0 -0
  116. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/tool.py +0 -0
  117. {dify_plugin-0.2.0/dify_plugin/tool → dify_plugin-0.2.2/dify_plugin/invocations/workflow_node}/__init__.py +0 -0
  118. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/workflow_node/parameter_extractor.py +0 -0
  119. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/invocations/workflow_node/question_classifier.py +0 -0
  120. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/plugin.py +0 -0
  121. {dify_plugin-0.2.0/tests → dify_plugin-0.2.2/dify_plugin/tool}/__init__.py +0 -0
  122. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/dify_plugin/tool/entities.py +0 -0
  123. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/entities/models/test_llm.py +0 -0
  124. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/interfaces/agent/test_agent.py +0 -0
  125. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/invocations/test_storage.py +0 -0
  126. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/test_llm_result.py +0 -0
  127. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/test_prompt_message.py +0 -0
  128. {dify_plugin-0.2.0 → dify_plugin-0.2.2}/tests/test_tool_call_model_init.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dify_plugin
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Dify Plugin SDK
5
5
  Keywords: dify,plugin,sdk
6
6
  Author-Email: langgenius <hello@dify.ai>
@@ -43,6 +43,6 @@ When depending on this SDK, it's recommended to specify version constraints that
43
43
  Example in your project's dependency management:
44
44
 
45
45
  ```
46
- dify_plugin>=0.1.0,<0.2.0
46
+ dify_plugin>=0.2.0,<0.3.0
47
47
  ```
48
48
 
@@ -19,6 +19,6 @@ When depending on this SDK, it's recommended to specify version constraints that
19
19
  Example in your project's dependency management:
20
20
 
21
21
  ```
22
- dify_plugin>=0.1.0,<0.2.0
22
+ dify_plugin>=0.2.0,<0.3.0
23
23
  ```
24
24
 
@@ -0,0 +1,16 @@
1
+ import argparse
2
+
3
+ from dify_plugin.commands.generate_docs import generate_docs
4
+
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(description="Dify Plugin SDK Documentation Generator")
8
+ parser.add_argument("command", choices=["generate-docs"], help="Command to run")
9
+ args = parser.parse_args()
10
+
11
+ if args.command == "generate-docs":
12
+ generate_docs()
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
@@ -0,0 +1,3 @@
1
+ from dify_plugin.commands.generate_docs import generate_docs
2
+
3
+ __all__ = ["generate_docs"]
@@ -0,0 +1,5 @@
1
+ from dify_plugin.core.documentation.generator import SchemaDocumentationGenerator
2
+
3
+
4
+ def generate_docs():
5
+ SchemaDocumentationGenerator().generate_docs("docs.md")
@@ -0,0 +1,416 @@
1
+ from collections import defaultdict
2
+ from enum import Enum
3
+ from typing import Any, Union
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from dify_plugin.core.documentation.schema_doc import list_schema_docs
8
+ from dify_plugin.core.entities import * # noqa: F403
9
+ from dify_plugin.core.entities.plugin import * # noqa: F403
10
+ from dify_plugin.core.entities.plugin.setup import * # noqa: F403
11
+ from dify_plugin.entities import * # noqa: F403
12
+ from dify_plugin.entities.agent import * # noqa: F403
13
+ from dify_plugin.entities.endpoint import * # noqa: F403
14
+ from dify_plugin.entities.model import * # noqa: F403
15
+ from dify_plugin.entities.model.llm import * # noqa: F403
16
+ from dify_plugin.entities.model.moderation import * # noqa: F403
17
+ from dify_plugin.entities.model.provider import * # noqa: F403
18
+ from dify_plugin.entities.model.rerank import * # noqa: F403
19
+ from dify_plugin.entities.model.speech2text import * # noqa: F403
20
+ from dify_plugin.entities.model.text_embedding import * # noqa: F403
21
+ from dify_plugin.entities.model.tts import * # noqa: F403
22
+ from dify_plugin.entities.tool import * # noqa: F403
23
+
24
+
25
+ class SchemaDocumentationGenerator:
26
+ def __init__(self):
27
+ self._reference_counts: dict[type, int] = {}
28
+ self._reference_graph: dict[type, set[type]] = defaultdict(set)
29
+ self._processed_types: set[type] = set()
30
+ self._field_descriptions: dict[tuple[type, str], str] = {}
31
+ self._schema_descriptions: dict[type, str] = {}
32
+ self._processed_field_types: set[type] = set()
33
+ self._type_to_schema: dict[type, Any] = {}
34
+ self._type_blocks: dict[type, int] = {}
35
+ self._blocks: list[list] = []
36
+ self._types: set[type] = set()
37
+
38
+ def _organize_toc(self) -> list[tuple[type, list[Any]]]:
39
+ """Organize types into a hierarchical structure for table of contents.
40
+
41
+ The hierarchy is built based on the following rules:
42
+ 1. Types marked with top=True are placed at the root level first
43
+ 2. Types referenced by multiple other types are placed at the root level
44
+ 3. Types not referenced by any other type are placed at the root level
45
+ 4. Types referenced by exactly one other type are placed as children of their parent type
46
+ 5. This process continues recursively for each child type
47
+
48
+ This ensures that:
49
+ - Important types (marked with top=True) are easily accessible
50
+ - Types that are part of multiple other types are at root for easy access
51
+ - Types that belong to a single parent are properly nested
52
+ - The hierarchy reflects the actual reference relationships in the code
53
+ - Deep reference chains are properly represented (A -> B -> C shown as nested structure)
54
+
55
+ Returns:
56
+ List[Tuple[Type, List[Any]]]: A list of tuples, where each tuple contains:
57
+ - A parent type
58
+ - A list of its child nodes, each being a tuple of (Type, List[Any])
59
+ """
60
+ # Build a reverse reference map: type -> set of types that reference it
61
+ referenced_by = {t: set() for t in self._types}
62
+ for t, refs in self._reference_graph.items():
63
+ for ref in refs:
64
+ if ref in referenced_by:
65
+ referenced_by[ref].add(t)
66
+
67
+ def build_subtree(type_: type, processed: set[type]) -> tuple[type, list[Any]]:
68
+ """Recursively build a subtree for a type and its references.
69
+
70
+ Args:
71
+ type_: The type to build a subtree for
72
+ processed: Set of already processed types to avoid cycles
73
+
74
+ Returns:
75
+ Tuple[Type, List[Any]]: The type and its nested children
76
+ """
77
+ if type_ in processed:
78
+ return type_, []
79
+
80
+ processed.add(type_)
81
+ children = []
82
+
83
+ # Find all types that are only referenced by this type
84
+ for ref_type in self._reference_graph.get(type_, set()):
85
+ # If this is the only reference to ref_type
86
+ refs = referenced_by.get(ref_type, set())
87
+ if len(refs) == 1 and next(iter(refs)) == type_:
88
+ subtree = build_subtree(ref_type, processed)
89
+ children.append(subtree)
90
+
91
+ return type_, children
92
+
93
+ # Start building the hierarchy
94
+ hierarchy = []
95
+ processed = set()
96
+
97
+ # Phase 1: Add types marked with top=True at the root level
98
+ for t in self._types:
99
+ if t not in processed and hasattr(t, "__schema_docs__") and any(doc.top for doc in t.__schema_docs__):
100
+ subtree = build_subtree(t, processed)
101
+ hierarchy.append(subtree)
102
+
103
+ # Phase 2: Add types that are not referenced by any other type
104
+ # or are referenced by multiple types
105
+ remaining = [t for t in self._types if len(referenced_by[t]) != 1]
106
+ for t in remaining:
107
+ if t not in processed:
108
+ subtree = build_subtree(t, processed)
109
+ hierarchy.append(subtree)
110
+
111
+ # Phase 3: Add any remaining types that weren't processed
112
+ for t in self._types:
113
+ if t not in processed:
114
+ subtree = build_subtree(t, processed)
115
+ hierarchy.append(subtree)
116
+
117
+ return hierarchy
118
+
119
+ def generate_docs(self, output_file: str):
120
+ with open(output_file, "w") as f:
121
+ # Write header
122
+ f.write("# Dify Plugin SDK Schema Documentation\n\n")
123
+
124
+ schemas = list_schema_docs()
125
+
126
+ # Build type to schema mapping
127
+ for schema in schemas:
128
+ self._type_to_schema[schema.cls] = schema
129
+ self._types.add(schema.cls)
130
+
131
+ # Pre-process schemas to collect field descriptions
132
+ self._preprocess_schemas(schemas)
133
+
134
+ # Count references and build reference graph
135
+ self._build_reference_graph(schemas)
136
+
137
+ # Create blocks
138
+ self._create_blocks()
139
+
140
+ # Generate table of contents
141
+ f.write("## Table of Contents\n\n")
142
+ hierarchy = self._organize_toc()
143
+
144
+ def write_toc_item(node: tuple[type, list[Any]], indent: int = 0):
145
+ type_, children = node
146
+ schema = self._type_to_schema[type_]
147
+ name = schema.name or type_.__name__
148
+ f.write(f"{' ' * (indent * 2)}- [{name}](#{name.lower()})\n")
149
+ for child in children:
150
+ write_toc_item(child, indent + 1)
151
+
152
+ for node in hierarchy:
153
+ write_toc_item(node)
154
+ f.write("\n")
155
+
156
+ # Generate documentation for each block
157
+ for block in self._blocks:
158
+ for type_ in block:
159
+ self._write_schema_doc(f, type_)
160
+
161
+ def _preprocess_schemas(self, schemas: list) -> None:
162
+ """Pre-process schemas to collect field descriptions and merge duplicates."""
163
+ # First pass: collect all field descriptions
164
+ for schema in schemas:
165
+ cls = schema.cls
166
+ if not issubclass(cls, BaseModel):
167
+ continue
168
+
169
+ # Store schema description
170
+ if cls not in self._schema_descriptions or len(schema.description) > len(self._schema_descriptions[cls]):
171
+ self._schema_descriptions[cls] = schema.description
172
+
173
+ # Store field descriptions
174
+ outside_reference_fields = getattr(schema, "outside_reference_fields", {}) or {}
175
+ for field_name, field_info in cls.model_fields.items():
176
+ field_type = field_info.annotation
177
+ if field_type is None:
178
+ continue
179
+
180
+ # For BaseModel types that are not outside references, we'll document them separately
181
+ if (
182
+ isinstance(field_type, type)
183
+ and issubclass(field_type, BaseModel)
184
+ and field_name not in outside_reference_fields
185
+ ):
186
+ continue
187
+
188
+ key = (cls, field_name)
189
+ description = field_info.description or ""
190
+
191
+ # Handle dynamic fields
192
+ if hasattr(schema, "dynamic_fields") and schema.dynamic_fields and field_name in schema.dynamic_fields:
193
+ description = schema.dynamic_fields[field_name]
194
+
195
+ # For outside reference fields, append reference information to description
196
+ if field_name in outside_reference_fields:
197
+ referenced_type = outside_reference_fields[field_name]
198
+ referenced_schema = self._type_to_schema.get(referenced_type)
199
+ schema_name = referenced_schema.name if referenced_schema else referenced_type.__name__
200
+ if description:
201
+ description = f"{description} "
202
+ f"(Paths to yaml files that will be loaded as [{schema_name}](#{schema_name.lower()}))"
203
+ else:
204
+ description = (
205
+ f"Paths to yaml files that will be loaded as [{schema_name}](#{schema_name.lower()})"
206
+ )
207
+
208
+ # Store the most detailed description
209
+ if key not in self._field_descriptions or len(description) > len(self._field_descriptions[key]):
210
+ self._field_descriptions[key] = description
211
+
212
+ def _extract_referenced_types(self, field_type):
213
+ """Recursively extract all referenced BaseModel and Enum types from a field type."""
214
+ referenced = set()
215
+ if field_type is None:
216
+ return referenced
217
+
218
+ # Handle direct type references (BaseModel and Enum)
219
+ if isinstance(field_type, type):
220
+ if issubclass(field_type, (BaseModel, Enum)):
221
+ referenced.add(field_type)
222
+ # Handle generic types (List, Dict, Union, etc)
223
+ elif (hasattr(field_type, "__origin__") and field_type.__origin__ == Union) or hasattr(field_type, "__args__"):
224
+ # Handle Union types
225
+ for arg in field_type.__args__:
226
+ referenced.update(self._extract_referenced_types(arg))
227
+
228
+ return referenced
229
+
230
+ def _build_reference_graph(self, schemas: list) -> None:
231
+ """Build a graph of references between types (recursively for all nested types)."""
232
+ for schema in schemas:
233
+ cls = schema.cls
234
+ if not issubclass(cls, BaseModel):
235
+ continue
236
+
237
+ # Count references in fields
238
+ for field_name, field_info in cls.model_fields.items():
239
+ field_type = field_info.annotation
240
+ if field_type is None:
241
+ continue
242
+
243
+ # Handle outside reference fields
244
+ outside_reference_fields = getattr(schema, "outside_reference_fields", {}) or {}
245
+ if field_name in outside_reference_fields:
246
+ referenced_type = outside_reference_fields[field_name]
247
+ # Add the reference to the graph
248
+ self._reference_graph[cls].add(referenced_type)
249
+ self._reference_counts[referenced_type] = self._reference_counts.get(referenced_type, 0) + 1
250
+ continue
251
+
252
+ for ref_type in self._extract_referenced_types(field_type):
253
+ if ref_type != cls: # Avoid self-references
254
+ self._reference_graph[cls].add(ref_type)
255
+ self._reference_counts[ref_type] = self._reference_counts.get(ref_type, 0) + 1
256
+
257
+ def _create_blocks(self) -> None:
258
+ """Create documentation blocks for all types"""
259
+ # First pass: assign each type to a block index
260
+ for type_ in self._types:
261
+ if type_ not in self._type_blocks:
262
+ # If type has top=True, assign it to block 0
263
+ if hasattr(type_, "__schema_docs__") and any(doc.top for doc in type_.__schema_docs__):
264
+ self._type_blocks[type_] = 0
265
+ else:
266
+ # Assign to a new block, starting from 1
267
+ self._type_blocks[type_] = len(self._type_blocks) + 1
268
+
269
+ # Second pass: create actual blocks
270
+ # Initialize blocks list with enough empty lists
271
+ max_block_index = max(self._type_blocks.values()) if self._type_blocks else 0
272
+ self._blocks = [[] for _ in range(max_block_index + 1)]
273
+
274
+ for type_, block_index in self._type_blocks.items():
275
+ self._blocks[block_index].append(type_)
276
+
277
+ # Sort blocks to ensure top types are first
278
+ # Only move block 0 to the front if it contains top types
279
+ if (
280
+ self._blocks
281
+ and self._blocks[0]
282
+ and any(
283
+ hasattr(t, "__schema_docs__") and any(doc.top for doc in t.__schema_docs__) for t in self._blocks[0]
284
+ )
285
+ ):
286
+ top_block = self._blocks[0]
287
+ self._blocks.sort(key=lambda block: 0 if block is top_block else 1)
288
+
289
+ def _is_container_type(self, field_type: Any, container_types=(list, set)) -> bool:
290
+ """Check if a field type is a container type (list, set, etc)."""
291
+ try:
292
+ return (
293
+ hasattr(field_type, "__origin__")
294
+ and isinstance(getattr(field_type, "__origin__", None), type)
295
+ and getattr(field_type, "__origin__", None) in container_types
296
+ )
297
+ except Exception:
298
+ return False
299
+
300
+ def _get_container_name(self, field_type: Any) -> str:
301
+ """Get the name of a container type."""
302
+ try:
303
+ origin = getattr(field_type, "__origin__", None)
304
+ return origin.__name__ if origin else str(field_type)
305
+ except Exception:
306
+ return str(field_type)
307
+
308
+ def _write_schema_doc(self, f, type_) -> None:
309
+ """Write documentation for a single schema."""
310
+ schema = self._type_to_schema[type_]
311
+ name = schema.name or type_.__name__
312
+
313
+ f.write(f"## {name}\n\n")
314
+
315
+ # Write description
316
+ description = self._schema_descriptions.get(type_, "")
317
+ f.write(f"{description}\n\n")
318
+
319
+ if issubclass(type_, BaseModel):
320
+ f.write("### Fields\n\n")
321
+ f.write("| Name | Type | Description | Default | Extra |\n")
322
+ f.write("|------|------|-------------|---------|---------|\n")
323
+
324
+ # Track processed fields to avoid duplicates
325
+ processed_fields = set()
326
+ ignore_fields = set(getattr(schema, "ignore_fields", []) or [])
327
+ outside_reference_fields = getattr(schema, "outside_reference_fields", {}) or {}
328
+
329
+ for field_name, field_info in type_.model_fields.items():
330
+ if field_name in ignore_fields:
331
+ continue
332
+ field_type = field_info.annotation
333
+ if field_type is None:
334
+ continue
335
+
336
+ # Skip if we've already processed this field type
337
+ if isinstance(field_type, type) and issubclass(field_type, BaseModel):
338
+ if field_type in self._processed_field_types:
339
+ continue
340
+ self._processed_field_types.add(field_type)
341
+
342
+ # Skip if we've already processed this field
343
+ field_key = (field_type, field_name)
344
+ if field_key in processed_fields:
345
+ continue
346
+ processed_fields.add(field_key)
347
+
348
+ # Get the most detailed description
349
+ description = self._field_descriptions.get((type_, field_name), field_info.description or "")
350
+
351
+ # Format type name
352
+ type_name = self._format_type_name(field_type)
353
+
354
+ # Handle outside reference fields
355
+ if field_name in outside_reference_fields:
356
+ if self._is_container_type(field_type):
357
+ type_name = f"{self._get_container_name(field_type)}[str]"
358
+ else:
359
+ type_name = "str"
360
+
361
+ # Get field metadata
362
+ default = field_info.default
363
+ # User-friendly default value
364
+ if str(default) == "PydanticUndefined":
365
+ default = ""
366
+
367
+ # Get pattern if exists (robust)
368
+ extra = ""
369
+ if hasattr(field_info, "metadata"):
370
+ for value in field_info.metadata:
371
+ extra += f"{value} "
372
+
373
+ f.write(f"| {field_name} | {type_name} | {description} | {default} | {extra} |\n")
374
+
375
+ f.write("\n")
376
+
377
+ elif issubclass(type_, Enum):
378
+ f.write("### Values\n\n")
379
+ for member in type_:
380
+ f.write(f"- `{member.name}`: {member.value}\n")
381
+ f.write("\n")
382
+
383
+ def _format_type_name(self, field_type: Any) -> str:
384
+ """Format the type name for display, handling complex types and references.
385
+
386
+ For BaseModel and Enum types, use their schema name if available.
387
+ For container types (list, dict, etc), recursively format their type arguments.
388
+ """
389
+ if field_type is None:
390
+ return "Any"
391
+
392
+ if isinstance(field_type, type):
393
+ if issubclass(field_type, (BaseModel, Enum)):
394
+ # Use schema name if available
395
+ schema = self._type_to_schema.get(field_type)
396
+ name = schema.name if schema else field_type.__name__
397
+ return f"[{name}](#{name.lower()})"
398
+ return field_type.__name__
399
+
400
+ if hasattr(field_type, "__origin__") and hasattr(field_type, "__args__"):
401
+ origin = field_type.__origin__
402
+ if origin in (list, set):
403
+ inner_type = self._format_type_name(field_type.__args__[0])
404
+ return f"{origin.__name__}[{inner_type}]"
405
+ elif origin is dict:
406
+ key_type = self._format_type_name(field_type.__args__[0])
407
+ value_type = self._format_type_name(field_type.__args__[1])
408
+ return f"dict[{key_type}, {value_type}]"
409
+ elif origin is tuple:
410
+ types = [self._format_type_name(arg) for arg in field_type.__args__]
411
+ return f"tuple[{', '.join(types)}]"
412
+ elif origin is Union:
413
+ types = [self._format_type_name(arg) for arg in field_type.__args__]
414
+ return f"Union[{', '.join(types)}]"
415
+
416
+ return str(field_type)
@@ -0,0 +1,78 @@
1
+ from collections.abc import Callable, Mapping
2
+ from typing import Any, Optional
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class SchemaDoc:
8
+ def __init__(
9
+ self,
10
+ cls: type[BaseModel],
11
+ description: str,
12
+ name: Optional[str] = None,
13
+ top: bool = False,
14
+ ignore_fields: Optional[list[str]] = None,
15
+ outside_reference_fields: Optional[Mapping[str, type[BaseModel]]] = None,
16
+ ):
17
+ self.cls = cls
18
+ self.description = description
19
+ self.name = name
20
+ self.top = top
21
+ self.ignore_fields = ignore_fields or []
22
+ self.outside_reference_fields = outside_reference_fields or {}
23
+
24
+
25
+ __cls_mapping__: dict[type[BaseModel], SchemaDoc] = {}
26
+
27
+
28
+ def docs(
29
+ description: str,
30
+ name: Optional[str] = None,
31
+ top: bool = False,
32
+ ignore_fields: Optional[list[str]] = None,
33
+ outside_reference_fields: Optional[Mapping[str, type[BaseModel]]] = None,
34
+ ) -> Callable:
35
+ """
36
+ Decorator to add schema documentation to a class
37
+
38
+ Args:
39
+ description: Description of the schema
40
+ name: Optional name override for the schema
41
+ example: Optional example instance
42
+ reference: Optional reference to another schema
43
+ dynamic_fields: Optional dynamic field descriptions
44
+ top: Whether this schema should be placed at the top of the documentation
45
+ ignore_fields: List of field names to ignore in documentation
46
+ """
47
+
48
+ def decorator(cls_or_func: Any) -> Any:
49
+ # check if cls_or_func is a class
50
+ if isinstance(cls_or_func, type):
51
+ nonlocal name
52
+ name = name or cls_or_func.__name__
53
+
54
+ if cls_or_func not in __cls_mapping__:
55
+ __cls_mapping__[cls_or_func] = SchemaDoc(
56
+ cls_or_func, description, name, top, ignore_fields, outside_reference_fields
57
+ )
58
+
59
+ if not hasattr(cls_or_func, "__schema_docs__"):
60
+ cls_or_func.__schema_docs__ = []
61
+ cls_or_func.__schema_docs__.append(__cls_mapping__[cls_or_func])
62
+ return cls_or_func
63
+
64
+ return decorator
65
+
66
+
67
+ def get_schema_doc(cls: type[BaseModel]) -> SchemaDoc | None:
68
+ """
69
+ Get the schema documentation for a class
70
+ """
71
+ return __cls_mapping__.get(cls)
72
+
73
+
74
+ def list_schema_docs() -> list[SchemaDoc]:
75
+ """
76
+ List all schema documentation
77
+ """
78
+ return list(__cls_mapping__.values())