Repository: marcosschroh/dataclasses-avroschema

Issue:
Support for python class to be generated for enum type AVRO schema (without `fields`)
**Is your feature request related to a problem? Please describe.**
I have a valid AVRO schema which I am unable to translate into a Python class.
Example:
```
{
    "type": "enum",
    "name": "Color",
    "symbols": [
      "red",
      "blue"
    ]
}
```
running the following fails:
```
from dataclasses_avroschema import ModelGenerator, ModelType
schema = {
    "type": "enum",
    "name": "Color",
    "symbols": [
      "red",
      "blue"
    ]
} # this is actually loaded from the AVSC file. Pasting the dict for simplicity.
model_generator = ModelGenerator()
python_class = model_generator.render(schema=schema, model_type=ModelType.DATACLASS.value)
```
with the error:
```
  File "<location>/.venv/lib/python3.9/site-packages/dataclasses_avroschema/model_generator/generator.py", line 67, in render
    return self.render_module(
  File "<location>/.venv/lib/python3.9/site-packages/dataclasses_avroschema/model_generator/generator.py", line 97, in render_module
    return model_generator.render(schemas=schemas)
  File "<location>/.venv/lib/python3.9/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 66, in render
    classes = "\n".join(self.render_class(schema=schema) for schema in schemas)
  File "<location>/.venv/lib/python3.9/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 66, in <genexpr>
    classes = "\n".join(self.render_class(schema=schema) for schema in schemas)
  File "<location>/.venv/lib/python3.9/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 143, in render_class
    record_fields: typing.List[JsonDict] = schema["fields"]
KeyError: 'fields'
```
**Describe the solution you'd like**
It would be great to support all valid AVRO schemas to be supported and converted to python classes, also when `fields` is missing

**Describe alternatives you've considered**
-

**Additional context**
Add any other context or screenshots about the feature request here.


Discussion:
github-actions[bot]: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.4.7
:---:
:rocket: Deployed preview to https://marcosschroh.github.io/dataclasses-avroschema/pr-preview/pr-690/
on branch [`gh-pages`](https://github.com/marcosschroh/dataclasses-avroschema/tree/gh-pages) at 2024-07-24 11:56 UTC
<!-- Sticky Pull Request Commentpr-preview -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/marcosschroh/dataclasses-avroschema/pull/690?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.43%. Comparing base [(`ebaf5ba`)](https://app.codecov.io/gh/marcosschroh/dataclasses-avroschema/commit/ebaf5ba48a139ff1b550b5de3ca8d2e99af59bc4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh) to head [(`41582e2`)](https://app.codecov.io/gh/marcosschroh/dataclasses-avroschema/commit/41582e20dfcdf0d741226bcc40c3232c7e8739e1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #690      +/-   ##
==========================================
- Coverage   99.48%   99.43%   -0.05%     
==========================================
  Files          34       34              
  Lines        1927     1939      +12     
==========================================
+ Hits         1917     1928      +11     
- Misses         10       11       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/marcosschroh/dataclasses-avroschema/pull/690?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh).


Patch:
diff --git a/dataclasses_avroschema/model_generator/generator.py b/dataclasses_avroschema/model_generator/generator.py
index 90f49e4..2276f3d 100644
--- a/dataclasses_avroschema/model_generator/generator.py
+++ b/dataclasses_avroschema/model_generator/generator.py
@@ -29,6 +29,59 @@ class ModelType(str, enum.Enum):
 
 @dataclass
 class ModelGenerator:
+    """
+    ModelGenerator converts an avro schema to classes
+
+    **`Avro schema` --> `Python class`**
+
+    This class will be in charge of render all the python types in a proper way.
+    The rendered result is a string that contains proper identation, decorators, imports
+    and any extras so the result can be saved in a file and it will be ready to use.
+
+    !!! Example
+        ```python
+        from dataclasses_avroschema import ModelGenerator
+
+        model_generator = ModelGenerator()
+
+        schema = {
+            "type": "record",
+            "namespace": "com.kubertenes",
+            "name": "AvroDeployment",
+            "fields": [
+                {"name": "image", "type": "string"},
+                {"name": "replicas", "type": "int"},
+                {"name": "port", "type": "int"},
+            ],
+        }
+
+        result = model_generator.render(schema=schema)
+
+        # save the result in a file
+        with open("models.py", mode="+w") as f:
+            f.write(result)
+        ```
+
+    Then explore the module `models.py`, the result must be
+
+    ```python title="Code generated"
+    import dataclasses
+
+    from dataclasses_avroschema import AvroModel
+    from dataclasses_avroschema import types
+
+
+    @dataclasses.dataclass
+    class AvroDeployment(AvroModel):
+        image: str
+        replicas: types.Int32
+        port: types.Int32
+
+        class Meta:
+            namespace = "com.kubertenes"
+    ```
+    """
+
     base_class: str = BaseClassEnum.AVRO_MODEL.value
     include_original_schema: bool = False
     model_type_mapper: typing.Dict[str, BaseGenerator] = field(init=False)
diff --git a/dataclasses_avroschema/model_generator/lang/python/avrodantic/avrodantic_model_generator.py b/dataclasses_avroschema/model_generator/lang/python/avrodantic/avrodantic_model_generator.py
index 3ba26ca..01250a7 100644
--- a/dataclasses_avroschema/model_generator/lang/python/avrodantic/avrodantic_model_generator.py
+++ b/dataclasses_avroschema/model_generator/lang/python/avrodantic/avrodantic_model_generator.py
@@ -12,7 +12,6 @@ class AvroDanticModelGenerator(BaseGenerator):
         super().__post_init__()
 
         self.base_class = "AvroBaseModel"
-        self.imports.add("from dataclasses_avroschema.pydantic import AvroBaseModel")
         self.imports_dict = {
             "dataclass_field": "from pydantic import Field",
         }
@@ -49,3 +48,6 @@ class AvroDanticModelGenerator(BaseGenerator):
     def render_dataclass_field(self, properties: str) -> str:
         self.imports.add("from pydantic import Field")
         return super().render_dataclass_field(properties=properties)
+
+    def add_class_imports(self) -> None:
+        self.imports.add("from dataclasses_avroschema.pydantic import AvroBaseModel")
diff --git a/dataclasses_avroschema/model_generator/lang/python/base.py b/dataclasses_avroschema/model_generator/lang/python/base.py
index e70670f..2db178b 100644
--- a/dataclasses_avroschema/model_generator/lang/python/base.py
+++ b/dataclasses_avroschema/model_generator/lang/python/base.py
@@ -64,8 +64,21 @@ class BaseGenerator:
         self.avro_type_to_lang = avro_to_python_utils.AVRO_TYPE_TO_PYTHON
         self.logical_types_imports = avro_to_python_utils.LOGICAL_TYPES_IMPORTS
 
+    def add_class_imports(self) -> None: ...
+
+    def _resolve_type_from_metadata(self, *, field: JsonDict) -> typing.Optional[str]: ...
+
     def render(self, schemas: typing.List[JsonDict]) -> str:
-        classes = "\n".join(self.render_class(schema=schema) for schema in schemas)
+        result = []
+
+        for schema in schemas:
+            if "fields" in schema:
+                result.append(self.render_class(schema=schema))
+            else:
+                # If is not a Record then it is an Enum
+                self.render_field(field=schema, model_name="")
+
+        classes = "\n".join(result)
         imports = self.render_imports()
         extras = self.render_extras()
 
@@ -145,6 +158,7 @@ class BaseGenerator:
         """
         name: str = casefy.pascalcase(schema["name"])
         record_fields: typing.List[JsonDict] = schema["fields"]
+        self.add_class_imports()
 
         # Sort the fields according whether it has a default value
         fields_representation: typing.List[FieldRepresentation] = [
@@ -415,7 +429,6 @@ class BaseGenerator:
         We need a template for it in order to create the Enum
         """
         self.imports.add("import enum")
-
         enum_name: str = field["name"]
 
         symbols_map = {}
@@ -512,9 +525,6 @@ class BaseGenerator:
 
         return {name: value for name, value in field.items() if name not in keys_to_ignore}
 
-    def _resolve_type_from_metadata(self, *, field: JsonDict) -> typing.Optional[str]:
-        return None
-
     def get_field_default(
         self,
         *,
diff --git a/dataclasses_avroschema/model_generator/lang/python/dataclasses/dataclass_model_generator.py b/dataclasses_avroschema/model_generator/lang/python/dataclasses/dataclass_model_generator.py
index 87cc872..71d0631 100644
--- a/dataclasses_avroschema/model_generator/lang/python/dataclasses/dataclass_model_generator.py
+++ b/dataclasses_avroschema/model_generator/lang/python/dataclasses/dataclass_model_generator.py
@@ -2,6 +2,7 @@ from dataclasses import dataclass
 
 from dataclasses_avroschema.model_generator.lang.python import templates
 from dataclasses_avroschema.model_generator.lang.python.base import BaseGenerator
+from dataclasses_avroschema.types import JsonDict
 
 
 @dataclass
@@ -12,12 +13,16 @@ class DataclassModelGenerator(BaseGenerator):
         self.base_class = "AvroModel"
         # represent the decorator to add in the base class
         self.base_class_decorator = "@dataclasses.dataclass"
-
-        self.imports.add("import dataclasses")
-        self.imports.add("from dataclasses_avroschema import AvroModel")
         self.imports_dict = {
             "condecimal": "from dataclasses_avroschema.types import condecimal",
         }
 
         # Templates
         self.field_template = templates.field_template
+
+    def add_class_imports(self) -> None:
+        self.imports.add("import dataclasses")
+        self.imports.add("from dataclasses_avroschema import AvroModel")
+
+    def _resolve_type_from_metadata(self, *, field: JsonDict) -> None:
+        return None
diff --git a/dataclasses_avroschema/model_generator/lang/python/pydantic/pydantic_model_generator.py b/dataclasses_avroschema/model_generator/lang/python/pydantic/pydantic_model_generator.py
index 1d506b5..e805884 100644
--- a/dataclasses_avroschema/model_generator/lang/python/pydantic/pydantic_model_generator.py
+++ b/dataclasses_avroschema/model_generator/lang/python/pydantic/pydantic_model_generator.py
@@ -12,7 +12,6 @@ class PydanticModelGenerator(BaseGenerator):
         super().__post_init__()
 
         self.base_class = "BaseModel"
-        self.imports.add("from pydantic import BaseModel")
         self.imports_dict = {
             "dataclass_field": "from pydantic import Field",
         }
@@ -49,3 +48,6 @@ class PydanticModelGenerator(BaseGenerator):
     def render_dataclass_field(self, properties: str) -> str:
         self.imports.add("from pydantic import Field")
         return super().render_dataclass_field(properties=properties)
+
+    def add_class_imports(self) -> None:
+        self.imports.add("from pydantic import BaseModel")
diff --git a/dataclasses_avroschema/schema_generator.py b/dataclasses_avroschema/schema_generator.py
index f0f89f3..540d8e9 100644
--- a/dataclasses_avroschema/schema_generator.py
+++ b/dataclasses_avroschema/schema_generator.py
@@ -21,8 +21,8 @@ AVRO_JSON = "avro-json"
 CT = TypeVar("CT", bound="AvroModel")
 
 
-_schemas_cache: Dict['Type[AvroModel]', dict] = {}
-_dacite_config_cache: Dict['Type[AvroModel]', Config] = {}
+_schemas_cache: Dict["Type[AvroModel]", dict] = {}
+_dacite_config_cache: Dict["Type[AvroModel]", Config] = {}
 
 
 class AvroModel:
@@ -34,19 +34,19 @@ class AvroModel:
     _rendered_schema: OrderedDict = dataclasses.field(default_factory=OrderedDict)
 
     @classmethod
-    def generate_dataclass(cls: 'Type[CT]') -> 'Type[CT]':
+    def generate_dataclass(cls: "Type[CT]") -> "Type[CT]":
         if dataclasses.is_dataclass(cls):
             return cls  # type: ignore
         return dataclasses.dataclass(cls)
 
     @classmethod
-    def generate_metadata(cls: 'Type[CT]') -> SchemaMetadata:
+    def generate_metadata(cls: "Type[CT]") -> SchemaMetadata:
         meta = getattr(cls._klass, "Meta", type)
 
         return SchemaMetadata.create(meta)
 
     @classmethod
-    def generate_schema(cls: 'Type[CT]', schema_type: str = "avro") -> Optional[OrderedDict]:
+    def generate_schema(cls: "Type[CT]", schema_type: str = "avro") -> Optional[OrderedDict]:
         if cls._parser is None or cls.__mro__[1] != AvroModel:
             # Generate dataclass and metadata
             cls._klass = cls.generate_dataclass()
@@ -63,17 +63,17 @@ class AvroModel:
         return cls._rendered_schema
 
     @classmethod
-    def _generate_parser(cls: 'Type[CT]') -> Parser:
+    def _generate_parser(cls: "Type[CT]") -> Parser:
         cls._metadata = cls.generate_metadata()
         return Parser(type=cls._klass, metadata=cls._metadata, parent=cls._parent or cls)
 
     @classmethod
-    def avro_schema(cls: 'Type[CT]', case_type: Optional[str] = None, **kwargs) -> str:
+    def avro_schema(cls: "Type[CT]", case_type: Optional[str] = None, **kwargs) -> str:
         return json.dumps(cls.avro_schema_to_python(case_type=case_type), **kwargs)
 
     @classmethod
     def avro_schema_to_python(
-        cls: 'Type[CT]',
+        cls: "Type[CT]",
         parent: Optional["AvroModel"] = None,
         case_type: Optional[str] = None,
     ) -> Dict[str, Any]:
@@ -101,13 +101,13 @@ class AvroModel:
         return json.loads(json.dumps(avro_schema))
 
     @classmethod
-    def get_fields(cls: 'Type[CT]') -> List[Field]:
+    def get_fields(cls: "Type[CT]") -> List[Field]:
         if cls._parser is None:
             cls.generate_schema()
         return cls._parser.fields  # type: ignore
 
     @classmethod
-    def _reset_parser(cls: 'Type[CT]') -> None:
+    def _reset_parser(cls: "Type[CT]") -> None:
         """
         Reset all the values to original state.
         """
@@ -138,11 +138,11 @@ class AvroModel:
 
     @classmethod
     def deserialize(
-        cls: 'Type[CT]',
+        cls: "Type[CT]",
         data: bytes,
         serialization_type: str = AVRO,
         create_instance: bool = True,
-        writer_schema: Optional[Union[JsonDict, 'Type[CT]']] = None,
+        writer_schema: Optional[Union[JsonDict, "Type[CT]"]] = None,
     ) -> Union[JsonDict, CT]:
         payload = cls.deserialize_to_python(data, serialization_type, writer_schema)
         obj = cls.parse_obj(payload)
@@ -152,10 +152,10 @@ class AvroModel:
 
     @classmethod
     def deserialize_to_python(  # This can be used straight with a pydantic dataclass to bypass dacite
-        cls: 'Type[CT]',
+        cls: "Type[CT]",
         data: bytes,
         serialization_type: str = AVRO,
-        writer_schema: Union[JsonDict, 'Type[CT]', None] = None,
+        writer_schema: Union[JsonDict, "Type[CT]", None] = None,
     ) -> dict:
         if inspect.isclass(writer_schema) and issubclass(writer_schema, AvroModel):
             # mypy does not understand redefinitions
@@ -173,7 +173,7 @@ class AvroModel:
         )
 
     @classmethod
-    def parse_obj(cls: 'Type[CT]', data: Dict) -> CT:
+    def parse_obj(cls: "Type[CT]", data: Dict) -> CT:
         config = _dacite_config_cache.get(cls)
         if config is None:
             config = generate_dacite_config(cls)
@@ -194,7 +194,7 @@ class AvroModel:
         return json.dumps(data, **kwargs)
 
     @classmethod
-    def fake(cls: 'Type[CT]', **data: Any) -> CT:
+    def fake(cls: "Type[CT]", **data: Any) -> CT:
         """
         Creates a fake instance of the model.
 
diff --git a/docs/model_generator.md b/docs/model_generator.md
index 8eced49..e82d3c5 100644
--- a/docs/model_generator.md
+++ b/docs/model_generator.md
@@ -2,10 +2,11 @@
 
 This section describe how to convert `python classes` from an `avro schema` (avsc files). This is the inverse process that the library aims to.
 
-**`Avro schema` --> `Python class`**
-
-In order to create the models, we should use the `ModelGenerator` class. This class will be in charge of render all the python types in a proper way.
-The rendered result is a string that contains the proper identation, so the result can be saved in a file and it will be ready to use.
+::: dataclasses_avroschema.ModelGenerator
+    options:
+        show_source: false
+        members:
+          -  
 
 !!! note
     In future releases it will be possible to generate models for other programming langagues like `java` and `rust`
@@ -67,51 +68,6 @@ The rendered result is a string that contains the proper identation, so the resu
     | decimal | types.condecimal|
     | uuid | uuid.UUID    |
 
-## Usage
-
-```python
-from dataclasses_avroschema import ModelGenerator, ModelType
-
-model_generator = ModelGenerator()
-
-schema = {
-    "type": "record",
-    "namespace": "com.kubertenes",
-    "name": "AvroDeployment",
-    "fields": [
-        {"name": "image", "type": "string"},
-        {"name": "replicas", "type": "int"},
-        {"name": "port", "type": "int"},
-    ],
-}
-
-result = model_generator.render(schema=schema, model_type=ModelType.DATACLASS.value)
-
-# save the result in a file
-with open("models.py", mode="+w") as f:
-    f.write(result)
-```
-
-Then, the end result is:
-
-```python
-# models.py
-import dataclasses
-
-from dataclasses_avroschema import AvroModel
-from dataclasses_avroschema import types
-
-
-@dataclasses.dataclass
-class AvroDeployment(AvroModel):
-    image: str
-    replicas: types.Int32
-    port: types.Int32
-
-    class Meta:
-        namespace = "com.kubertenes"
-```
-
 ## Render a Python module
 
 It's also possible to generate a Python module containing classes from multiple schemas using `render_module`.
@@ -437,7 +393,7 @@ declared before required fields, which means that and invalid model will be gene
 For example the following schema contains the field `has_pets` (optional) before required fields:
 
 ```python
-from dataclasses_avroschema import ModelGenerator, ModelType
+from dataclasses_avroschema import ModelGenerator
 
 
 schema = {
@@ -453,7 +409,7 @@ schema = {
 }
 
 model_generator = ModelGenerator()
-result = model_generator.render(schema=schema, model_type=ModelType.DATACLASS.value)
+result = model_generator.render(schema=schema)
 
 # save the result in a file
 with open("models.py", mode="+w") as f:
@@ -482,7 +438,60 @@ class User(AvroModel):
         field_order = ['has_pets', 'name', 'age', 'money']
 ```
 
-## Enums and case sensitivity
+## Rendering Enums
+
+Because `avro enums` are represented by a python class, it is also possible to render them in isolation, for example:
+
+```python
+from dataclasses_avroschema import ModelGenerator
+
+
+enum_schema = {
+    "type": "enum",
+    "name": "Color",
+    "symbols": [
+        "red",
+        "blue",
+    ],
+    "default": "blue",
+}
+
+model_generator = ModelGenerator()
+result = model_generator.render(schema=schema)
+
+print(result)
+```
+
+Resulting in
+
+=== "python <= 3.10"
+    ```python
+    import enum
+
+
+    class Color(enum.Enum):
+        RED = "red"
+        BLUE = "blue"
+
+        class Meta:
+            default = "blue"
+    ```
+
+=== "python >= 3.11"
+    ```python
+    import enum
+
+
+    class Color(str, enum.Enum):
+        RED = "red"
+        BLUE = "blue"
+
+        @enum.nonmember
+        class Meta:
+            default = "blue"
+    ```
+
+### Enums and case sensitivity
 
 Sometimes there are schemas that contains the `symbols` which are case sensivity, for example `"symbols": ["P", "p"]`.
 Having something like that is NOT reccomended at all because it is meaninless, really hard to undestand the intention of it. Avoid it!!!


Test patch:
diff --git a/tests/model_generator/conftest.py b/tests/model_generator/conftest.py
index 986b94c..0a0195f 100644
--- a/tests/model_generator/conftest.py
+++ b/tests/model_generator/conftest.py
@@ -308,6 +308,19 @@ def schema_with_enum_types_with_inner_default() -> Dict:
     }
 
 
+@pytest.fixture
+def schema_enum_in_isolation() -> Dict:
+    return {
+        "type": "enum",
+        "name": "Color",
+        "symbols": [
+            "red",
+            "blue",
+        ],
+        "default": "blue",
+    }
+
+
 @pytest.fixture
 def schema_with_enum_types_case_sensitivity() -> Dict:
     return {
diff --git a/tests/model_generator/test_model_generator.py b/tests/model_generator/test_model_generator.py
index b22882d..ae69a6c 100644
--- a/tests/model_generator/test_model_generator.py
+++ b/tests/model_generator/test_model_generator.py
@@ -294,6 +294,24 @@ class User(AvroModel):
     assert result.strip() == expected_result.strip()
 
 
+def test_enum_in_isolation(schema_enum_in_isolation: types.JsonDict) -> None:
+    expected_result = f"""
+import enum
+
+
+class Color({templates.ENUM_PYTHON_VERSION}):
+    RED = "red"
+    BLUE = "blue"
+
+    {templates.METACLASS_DECORATOR}
+    class Meta:
+        default = "blue"
+"""
+    model_generator = ModelGenerator()
+    result = model_generator.render(schema=schema_enum_in_isolation)
+    assert result.strip() == expected_result.strip()
+
+
 def test_schema_with_custom_inner_names(schema_with_custom_inner_names: types.JsonDict) -> None:
     expected_result = """
 from dataclasses_avroschema import AvroModel