snakeboot 0.2.0__tar.gz → 0.3.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.
Files changed (47) hide show
  1. {snakeboot-0.2.0 → snakeboot-0.3.0}/PKG-INFO +12 -10
  2. {snakeboot-0.2.0 → snakeboot-0.3.0}/README.md +11 -9
  3. {snakeboot-0.2.0 → snakeboot-0.3.0}/pyproject.toml +1 -1
  4. {snakeboot-0.2.0 → snakeboot-0.3.0}/pyproject.toml.orig +1 -1
  5. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/configuration/__init__.py +8 -2
  6. snakeboot-0.3.0/src/snakeboot/configuration/cache_properties.py +16 -0
  7. snakeboot-0.3.0/src/snakeboot/configuration/database_properties.py +29 -0
  8. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/configuration/django_properties.py +3 -4
  9. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/configuration/properties_rules.py +2 -2
  10. snakeboot-0.3.0/src/snakeboot/configuration/section.py +12 -0
  11. {snakeboot-0.2.0 → snakeboot-0.3.0}/LICENSE +0 -0
  12. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/__init__.py +0 -0
  13. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/component_scan/__init__.py +0 -0
  14. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/component_scan/application_root.py +0 -0
  15. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/component_scan/component_scan.py +0 -0
  16. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/configuration/configuration.py +0 -0
  17. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/configuration/configuration_errors.py +0 -0
  18. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/configuration/configuration_properties.py +0 -0
  19. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/configuration/load_configuration.py +0 -0
  20. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/django/__init__.py +0 -0
  21. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/django/django_atomic_transaction_manager.py +0 -0
  22. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/events/__init__.py +0 -0
  23. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/events/application_event_listener.py +0 -0
  24. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/events/application_event_publisher.py +0 -0
  25. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/events/application_event_publisher_injectable.py +0 -0
  26. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/events/event_listener_markers.py +0 -0
  27. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/py.typed +0 -0
  28. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/__init__.py +0 -0
  29. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/authenticated_principal.py +0 -0
  30. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/controller_endpoint_factory.py +0 -0
  31. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/error_answer.py +0 -0
  32. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/rest_controller.py +0 -0
  33. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/rest_controller_advice.py +0 -0
  34. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/rest_controller_errors.py +0 -0
  35. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/rest_controller_mappings.py +0 -0
  36. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/rest_controller_mounter.py +0 -0
  37. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/rest_controller_scanner.py +0 -0
  38. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/rest/route_markers.py +0 -0
  39. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/testing/__init__.py +0 -0
  40. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/testing/declared_classes.py +0 -0
  41. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/testing/recording_transaction_manager.py +0 -0
  42. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/transaction/__init__.py +0 -0
  43. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/transaction/after_commit.py +0 -0
  44. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/transaction/transaction_manager.py +0 -0
  45. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/transaction/transaction_manager_not_installed_error.py +0 -0
  46. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/transaction/transaction_manager_registry.py +0 -0
  47. {snakeboot-0.2.0 → snakeboot-0.3.0}/src/snakeboot/transaction/transactional.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snakeboot
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Spring Boot ergonomics for hexagonal Python: @transactional, application events, a component scan, typed configuration with profiles and singleton REST controllers on Django Ninja, wired by wireup.
5
5
  Keywords: spring-boot,hexagonal-architecture,ddd,django,django-ninja,wireup,dependency-injection,transactional
6
6
  Author: Wouter Deketelaere
@@ -48,7 +48,8 @@ classes you can read in an afternoon, and none of them knows anything about your
48
48
  | `ApplicationEventPublisher`, `@event_listener`, `@transactional_event_listener` | the same names | wireup |
49
49
  | `@rest_controller`, `@route.get`, `@rest_controller_advice`, `@exception_handler` | `@RestController`, `@GetMapping`, `@RestControllerAdvice`, `@ExceptionHandler` | `snakeboot[ninja]` |
50
50
  | `DjangoAtomicTransactionManager` | `PlatformTransactionManager` for JDBC | `snakeboot[django]` |
51
- | `@configuration_properties`, `load_configuration`, `application-<profile>.toml` | `@ConfigurationProperties`, `@EnableConfigurationProperties`, profiles | `snakeboot[configuration]` |
51
+ | `@configuration_properties`, `Section`, `load_configuration`, `application-<profile>.toml` | `@ConfigurationProperties`, `@EnableConfigurationProperties`, profiles | `snakeboot[configuration]` |
52
+ | `DjangoProperties`, `DatabaseProperties`, `CacheProperties` | `server.*`, `spring.datasource.*`, `spring.data.redis.*` | `snakeboot[configuration]` |
52
53
  | `RecordingTransactionManager`, `declared_classes` | spring-test | nothing |
53
54
 
54
55
  The transaction, component scan and testing packages import nothing but the standard library,
@@ -76,22 +77,23 @@ class MyApplication: ...
76
77
  WIREUP = WireupSettings(injectables=[*ComponentScan.of(MyApplication).module_names()], auto_inject_views=False)
77
78
  ```
78
79
 
79
- **Typed configuration.** A Pydantic model is bound to one table of `application.toml`, like
80
- `@ConfigurationProperties`. Settings lists the sections once, like `@EnableConfigurationProperties`;
81
- every section then reaches wireup by its type.
80
+ **Typed configuration.** A `Section` (a frozen Pydantic model that refuses unknown keys) is bound to one
81
+ table of `application.toml`, like `@ConfigurationProperties`. Settings lists the sections once, like
82
+ `@EnableConfigurationProperties`; every section then reaches wireup by its type. The sections Django itself
83
+ needs come with snakeboot: `DjangoProperties` (`[django]`), `DatabaseProperties` (`[database]`, PostgreSQL) and
84
+ `CacheProperties` (`[cache]`, Redis).
82
85
 
83
86
  ```python
84
87
  @configuration_properties("chatbot")
85
- class ChatbotProperties(BaseModel):
86
- model_config = ConfigDict(extra="forbid", frozen=True)
87
-
88
+ class ChatbotProperties(Section):
88
89
  model: str # no default: the value is in application.toml
89
- llm: LanguageModelProperties # a nested model is the table [chatbot.llm]
90
+ llm: LanguageModelProperties # a nested Section is the table [chatbot.llm]
90
91
 
91
92
 
92
93
  # settings.py
93
- CONFIGURATION = load_configuration(DjangoProperties, ChatbotProperties, package="myapp")
94
+ CONFIGURATION = load_configuration(DjangoProperties, DatabaseProperties, ChatbotProperties, package="myapp")
94
95
  SECRET_KEY = CONFIGURATION.get(DjangoProperties).secret_key.get_secret_value()
96
+ DATABASES = {"default": CONFIGURATION.get(DatabaseProperties).as_django_database()}
95
97
  WIREUP = WireupSettings(injectables=[*ComponentScan.of(MyApplication).module_names(), *CONFIGURATION.injectables()])
96
98
  ```
97
99
 
@@ -13,7 +13,8 @@ classes you can read in an afternoon, and none of them knows anything about your
13
13
  | `ApplicationEventPublisher`, `@event_listener`, `@transactional_event_listener` | the same names | wireup |
14
14
  | `@rest_controller`, `@route.get`, `@rest_controller_advice`, `@exception_handler` | `@RestController`, `@GetMapping`, `@RestControllerAdvice`, `@ExceptionHandler` | `snakeboot[ninja]` |
15
15
  | `DjangoAtomicTransactionManager` | `PlatformTransactionManager` for JDBC | `snakeboot[django]` |
16
- | `@configuration_properties`, `load_configuration`, `application-<profile>.toml` | `@ConfigurationProperties`, `@EnableConfigurationProperties`, profiles | `snakeboot[configuration]` |
16
+ | `@configuration_properties`, `Section`, `load_configuration`, `application-<profile>.toml` | `@ConfigurationProperties`, `@EnableConfigurationProperties`, profiles | `snakeboot[configuration]` |
17
+ | `DjangoProperties`, `DatabaseProperties`, `CacheProperties` | `server.*`, `spring.datasource.*`, `spring.data.redis.*` | `snakeboot[configuration]` |
17
18
  | `RecordingTransactionManager`, `declared_classes` | spring-test | nothing |
18
19
 
19
20
  The transaction, component scan and testing packages import nothing but the standard library,
@@ -41,22 +42,23 @@ class MyApplication: ...
41
42
  WIREUP = WireupSettings(injectables=[*ComponentScan.of(MyApplication).module_names()], auto_inject_views=False)
42
43
  ```
43
44
 
44
- **Typed configuration.** A Pydantic model is bound to one table of `application.toml`, like
45
- `@ConfigurationProperties`. Settings lists the sections once, like `@EnableConfigurationProperties`;
46
- every section then reaches wireup by its type.
45
+ **Typed configuration.** A `Section` (a frozen Pydantic model that refuses unknown keys) is bound to one
46
+ table of `application.toml`, like `@ConfigurationProperties`. Settings lists the sections once, like
47
+ `@EnableConfigurationProperties`; every section then reaches wireup by its type. The sections Django itself
48
+ needs come with snakeboot: `DjangoProperties` (`[django]`), `DatabaseProperties` (`[database]`, PostgreSQL) and
49
+ `CacheProperties` (`[cache]`, Redis).
47
50
 
48
51
  ```python
49
52
  @configuration_properties("chatbot")
50
- class ChatbotProperties(BaseModel):
51
- model_config = ConfigDict(extra="forbid", frozen=True)
52
-
53
+ class ChatbotProperties(Section):
53
54
  model: str # no default: the value is in application.toml
54
- llm: LanguageModelProperties # a nested model is the table [chatbot.llm]
55
+ llm: LanguageModelProperties # a nested Section is the table [chatbot.llm]
55
56
 
56
57
 
57
58
  # settings.py
58
- CONFIGURATION = load_configuration(DjangoProperties, ChatbotProperties, package="myapp")
59
+ CONFIGURATION = load_configuration(DjangoProperties, DatabaseProperties, ChatbotProperties, package="myapp")
59
60
  SECRET_KEY = CONFIGURATION.get(DjangoProperties).secret_key.get_secret_value()
61
+ DATABASES = {"default": CONFIGURATION.get(DatabaseProperties).as_django_database()}
60
62
  WIREUP = WireupSettings(injectables=[*ComponentScan.of(MyApplication).module_names(), *CONFIGURATION.injectables()])
61
63
  ```
62
64
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "snakeboot"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Spring Boot ergonomics for hexagonal Python: @transactional, application events, a component scan, typed configuration with profiles and singleton REST controllers on Django Ninja, wired by wireup."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "snakeboot"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Spring Boot ergonomics for hexagonal Python: @transactional, application events, a component scan, typed configuration with profiles and singleton REST controllers on Django Ninja, wired by wireup."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,8 +1,7 @@
1
1
  """Technical library: typed configuration from application.toml, one profile and the environment.
2
2
 
3
3
  @configuration_properties("chatbot") # @ConfigurationProperties(prefix = "chatbot")
4
- class ChatbotProperties(BaseModel):
5
- model_config = ConfigDict(extra="forbid", frozen=True)
4
+ class ChatbotProperties(Section):
6
5
  model: str # no default: the value is in application.toml
7
6
 
8
7
  # in settings.py, like @EnableConfigurationProperties({DjangoProperties.class, ChatbotProperties.class}):
@@ -12,24 +11,31 @@
12
11
 
13
12
  The values come from `application.toml` in the package, then `application-<profile>.toml` when
14
13
  SNAKEBOOT_PROFILE is set, then environment variables such as CHATBOT__MODEL; nothing else.
14
+ `DjangoProperties`, `DatabaseProperties` and `CacheProperties` are the sections Django itself needs.
15
15
  """
16
16
 
17
+ from snakeboot.configuration.cache_properties import CacheProperties
17
18
  from snakeboot.configuration.configuration import Configuration
18
19
  from snakeboot.configuration.configuration_errors import (
19
20
  ConfigurationFileNotFoundError,
20
21
  InvalidConfigurationPropertiesError,
21
22
  )
22
23
  from snakeboot.configuration.configuration_properties import configuration_properties
24
+ from snakeboot.configuration.database_properties import DatabaseProperties
23
25
  from snakeboot.configuration.django_properties import DjangoProperties
24
26
  from snakeboot.configuration.load_configuration import BASE_FILE, PROFILE_VARIABLE, load_configuration
27
+ from snakeboot.configuration.section import Section
25
28
 
26
29
  __all__ = [
27
30
  "BASE_FILE",
28
31
  "PROFILE_VARIABLE",
32
+ "CacheProperties",
29
33
  "Configuration",
30
34
  "ConfigurationFileNotFoundError",
35
+ "DatabaseProperties",
31
36
  "DjangoProperties",
32
37
  "InvalidConfigurationPropertiesError",
38
+ "Section",
33
39
  "configuration_properties",
34
40
  "load_configuration",
35
41
  ]
@@ -0,0 +1,16 @@
1
+ from snakeboot.configuration.configuration_properties import configuration_properties
2
+ from snakeboot.configuration.section import Section
3
+
4
+
5
+ @configuration_properties("cache")
6
+ class CacheProperties(Section):
7
+ """The section `[cache]`: the Redis that backs Django's cache, like Spring Boot's `spring.data.redis.*`.
8
+
9
+ CACHES = {"default": CONFIGURATION.get(CacheProperties).as_django_cache()}
10
+ """
11
+
12
+ redis_url: str
13
+
14
+ def as_django_cache(self) -> dict[str, str]:
15
+ """The entry of Django's `CACHES` setting."""
16
+ return {"BACKEND": "django.core.cache.backends.redis.RedisCache", "LOCATION": self.redis_url}
@@ -0,0 +1,29 @@
1
+ from pydantic import SecretStr
2
+
3
+ from snakeboot.configuration.configuration_properties import configuration_properties
4
+ from snakeboot.configuration.section import Section
5
+
6
+
7
+ @configuration_properties("database")
8
+ class DatabaseProperties(Section):
9
+ """The section `[database]`: the PostgreSQL database of the service, like Spring Boot's `spring.datasource.*`.
10
+
11
+ DATABASES = {"default": CONFIGURATION.get(DatabaseProperties).as_django_database()}
12
+ """
13
+
14
+ host: str
15
+ port: int
16
+ name: str
17
+ user: str
18
+ password: SecretStr
19
+
20
+ def as_django_database(self) -> dict[str, str]:
21
+ """The entry of Django's `DATABASES` setting."""
22
+ return {
23
+ "ENGINE": "django.db.backends.postgresql",
24
+ "HOST": self.host,
25
+ "PORT": str(self.port),
26
+ "NAME": self.name,
27
+ "USER": self.user,
28
+ "PASSWORD": self.password.get_secret_value(),
29
+ }
@@ -1,19 +1,18 @@
1
1
  from typing import Literal
2
2
 
3
- from pydantic import BaseModel, ConfigDict, SecretStr
3
+ from pydantic import SecretStr
4
4
 
5
5
  from snakeboot.configuration.configuration_properties import configuration_properties
6
+ from snakeboot.configuration.section import Section
6
7
 
7
8
 
8
9
  @configuration_properties("django")
9
- class DjangoProperties(BaseModel):
10
+ class DjangoProperties(Section):
10
11
  """The section `[django]`: what settings.py hands to Django itself, like Spring Boot's `server.*` properties.
11
12
 
12
13
  `secret_key` is a SecretStr, so it never shows in a log or a traceback; settings.py unwraps it once.
13
14
  """
14
15
 
15
- model_config = ConfigDict(extra="forbid", frozen=True)
16
-
17
16
  secret_key: SecretStr
18
17
  debug: bool
19
18
  allowed_hosts: tuple[str, ...]
@@ -42,8 +42,8 @@ def _models_named_by(annotation: object) -> Iterator[type[BaseModel]]:
42
42
  def _refuse_open_model(model: type[BaseModel]) -> None:
43
43
  if model.model_config.get("extra") != "forbid" or model.model_config.get("frozen") is not True:
44
44
  raise InvalidConfigurationPropertiesError(
45
- f'{model.__name__} must declare model_config = ConfigDict(extra="forbid", frozen=True): an unknown '
46
- "key is a typo that must stop the start, and the configuration does not change while running."
45
+ f"{model.__name__} must extend Section (extra='forbid', frozen=True): an unknown key is a typo that "
46
+ "must stop the start, and the configuration does not change while running."
47
47
  )
48
48
 
49
49
 
@@ -0,0 +1,12 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+
4
+ class Section(BaseModel):
5
+ """A table of application.toml as a class: a frozen Pydantic model that refuses unknown keys.
6
+
7
+ Every section and every table nested in it extends `Section`, so a typo in a key stops the start and a value
8
+ cannot change while the application runs. Give no field a default: every value is written in application.toml
9
+ or in a profile, where everyone can read it.
10
+ """
11
+
12
+ model_config = ConfigDict(extra="forbid", frozen=True)
File without changes