Skip to content

Commit c4708a5

Browse files
committed
Finished documenting code and small update to README.md
1 parent abef750 commit c4708a5

File tree

52 files changed

+1649
-171
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1649
-171
lines changed

README.md

Lines changed: 77 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![Java: 17+](https://img.shields.io/badge/Java-17%2B-blue.svg)](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)
88
[![Spring Boot: 3.5.3+](https://img.shields.io/badge/Spring%20Boot-3.5.3%2B-brightgreen.svg)](https://spring.io/projects/spring-boot)
99
[![Python: 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
10-
![Tests Passed: 80%](https://img.shields.io/badge/Tests%20Passed-80%25-green.svg)
10+
![Tests Passed: 90%](https://img.shields.io/badge/Tests%20Passed-80%25-green.svg)
1111

1212
<hr>
1313

@@ -17,6 +17,7 @@
1717
- [Introduction](#-introduction)
1818
- [Features & Architecture](#-features--architecture)
1919
- [Core Components](#core-components)
20+
- [Architecture Diagram](#architecture-diagram)
2021
- [Security](#security)
2122
- [Cache](#cache)
2223
- [Installation and Setup](#-installation-and-setup)
@@ -76,64 +77,86 @@ Spring Boot Python Executor provides a flexible architecture for executing Pytho
7677
- Result resolution for capturing Python output
7778
- **PythonProcessor**: Connects resolvers and executors
7879

80+
### Architecture Diagram
81+
82+
If you are willing to use AOP based script processing,
83+
your Python code will move from Python annotation to a configured PythonProcessor implementation object
84+
7985
```mermaid
8086
---
81-
title: Basic Python script flow
87+
title: Python annotations flow
8288
---
8389
8490
classDiagram
85-
PythonBefore --> PythonProcessor: Annotation @PythonBefore forwards the script to the PythonProcessor implementation
86-
PythonAfter --> PythonProcessor: Annotation @PythonAfter forwards the script to the PythonProcessor implementation
87-
PythonProcessor --> PythonFileHandler: Firstly, the script is checked whether it's a .py file or String script
88-
PythonProcessor <-- PythonFileHandler: If it's a file, the content is read as a String value and returned
89-
PythonProcessor --> PythonResolverHolder: Secondly, the script may be transformed by multiple PythonResolver implementations. You can configure declared implementations by using spring.python.resolver.declared property
90-
PythonProcessor <-- PythonResolverHolder: Returns resolved script from multiple PythonResolvers
91-
PythonProcessor --> PythonExecutor: Finally, the script is executed by PythonExecutor implementation. You can choose needed implementation by configuring the property named spring.python.executor.type
92-
PythonProcessor <-- PythonExecutor: The method returns null, because of annotation usage, but if you would like to get specific result object you need to call process(...) function manually
93-
94-
class PythonBefore {
95-
+String value()
96-
+String script()
97-
+String[] activeProfiles()
98-
}
99-
100-
class PythonAfter {
101-
+String value()
102-
+String script()
103-
+String[] activeProfiles()
104-
}
105-
106-
class PythonProcessor {
107-
<<interface>>
108-
+R process(String script, Class<? extends R> resultClass, Map<String, Object> arguments)
109-
}
110-
111-
class PythonFileHandler {
112-
<<interface>>
113-
+boolean isPythonFile(String path)
114-
+void writeScriptBodyToFile(String path, String script)
115-
+void writeScriptBodyToFile(Path path, String script)
116-
+String readScriptBodyFromFile(String path)
117-
+String readScriptBodyFromFile(Path path)
118-
+String readScriptBodyFromFile(String path, UnaryOperator<String> mapper)
119-
+String readScriptBodyFromFile(Path path, UnaryOperator<String> mapper)
120-
+Path getScriptPath(String path)
121-
}
122-
123-
class PythonResolverHolder {
124-
<<interface>>
125-
+Iterator<PythonResolver> iterator()
126-
+void forEach(Consumer<? super PythonResolver> action)
127-
+Spliterator<PythonResolver> spliterator()
128-
+Stream<PythonResolver> stream()
129-
+String resolveAll(String script, Map<String, Object> arguments);
130-
+List<PythonResolver> getResolvers();
131-
}
132-
133-
class PythonExecutor {
134-
<<interface>>
135-
+R execute(String script, Class<? extends R> resultClass)
136-
}
91+
PythonBefores --> PythonAnnotationEvaluator
92+
PythonBefore --> PythonAnnotationEvaluator
93+
PythonAfters --> PythonAnnotationEvaluator
94+
PythonAfter --> PythonAnnotationEvaluator
95+
PythonAnnotationEvaluator --> PythonAnnotationValueCompounder
96+
PythonAnnotationEvaluator <-- PythonAnnotationValueCompounder
97+
PythonAnnotationEvaluator --> ProfileChecker
98+
PythonAnnotationEvaluator <-- ProfileChecker
99+
PythonAnnotationEvaluator --> PythonArgumentsExtractor
100+
PythonAnnotationEvaluator <-- PythonArgumentsExtractor
101+
PythonAnnotationEvaluator --> PythonProcessor
102+
PythonAnnotationValueCompounder --> PythonAnnotationValueExtractor
103+
PythonAnnotationValueCompounder <-- PythonAnnotationValueExtractor
104+
PythonAnnotationValueExtractor --> PythonMethodExtractor
105+
PythonAnnotationValueExtractor <-- PythonMethodExtractor
106+
PythonArgumentsExtractor --> PythonMethodExtractor
107+
PythonArgumentsExtractor <-- PythonMethodExtractor
108+
109+
110+
```
111+
112+
Also, you can use only PythonProcessor object to execute Python code:
113+
```mermaid
114+
---
115+
title: PythonProcessor flow
116+
---
117+
118+
classDiagram
119+
PythonProcessor --> PythonFileHandler: Firstly, the script is checked whether it's a .py file or String script
120+
PythonProcessor <-- PythonFileHandler: If it's a file, the content is read as a String value and returned
121+
PythonProcessor --> PythonResolverHolder: Secondly, the script may be transformed by multiple PythonResolver implementations. You can configure declared implementations by using spring.python.resolver.declared property
122+
PythonProcessor <-- PythonResolverHolder: Returns resolved script from multiple PythonResolvers
123+
PythonProcessor --> PythonExecutor: Finally, the script is executed by PythonExecutor implementation. You can choose needed implementation by configuring the property named spring.python.executor.type
124+
PythonProcessor <-- PythonExecutor: The method returns null, because of annotation usage, but if you would like to get specific result object you need to call process(...) function manually
125+
126+
class PythonProcessor {
127+
<<interface>>
128+
+void process(String script)
129+
+void process(String script, Map<String, Object> arguments)
130+
+R process(String script, Class<? extends R> resultClass)
131+
+R process(String script, Class<? extends R> resultClass, Map<String, Object> arguments)
132+
}
133+
134+
class PythonFileHandler {
135+
<<interface>>
136+
+boolean isPythonFile(String path)
137+
+void writeScriptBodyToFile(String path, String script)
138+
+void writeScriptBodyToFile(Path path, String script)
139+
+String readScriptBodyFromFile(String path)
140+
+String readScriptBodyFromFile(Path path)
141+
+String readScriptBodyFromFile(String path, UnaryOperator<String> mapper)
142+
+String readScriptBodyFromFile(Path path, UnaryOperator<String> mapper)
143+
+Path getScriptPath(String path)
144+
}
145+
146+
class PythonResolverHolder {
147+
<<interface>>
148+
+Iterator<PythonResolver> iterator()
149+
+void forEach(Consumer<? super PythonResolver> action)
150+
+Spliterator<PythonResolver> spliterator()
151+
+Stream<PythonResolver> stream()
152+
+String resolveAll(String script, Map<String, Object> arguments);
153+
+List<PythonResolver> getResolvers();
154+
}
155+
156+
class PythonExecutor {
157+
<<interface>>
158+
+R execute(String script, Class<? extends R> resultClass)
159+
}
137160
```
138161

139162
### Security

demo-app/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
<groupId>io.github.w4t3rcs</groupId>
2626
<artifactId>spring-boot-python-executor-cache-starter</artifactId>
2727
</dependency>
28+
<dependency>
29+
<groupId>io.github.w4t3rcs</groupId>
30+
<artifactId>spring-boot-python-executor-testcontainers</artifactId>
31+
</dependency>
2832

2933
<dependency>
3034
<groupId>org.springframework.boot</groupId>

demo-app/src/test/java/io/w4t3rcs/demo/DemoAppApplicationTests.java

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.w4t3rcs.demo;
2+
3+
import io.w4t3rcs.demo.dto.MLScriptRequest;
4+
import io.w4t3rcs.demo.service.PythonService;
5+
import org.junit.jupiter.api.Test;
6+
import org.mockito.Mockito;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.boot.test.context.SpringBootTest;
9+
import org.springframework.context.annotation.Import;
10+
11+
@SpringBootTest
12+
@Import(TestcontainersConfiguration.class)
13+
public class PythonServiceTests {
14+
@Autowired
15+
private PythonService pythonService;
16+
17+
@Test
18+
public void doExecuteWithPython() {
19+
MLScriptRequest request = Mockito.mock(MLScriptRequest.class);
20+
pythonService.doSomethingWithPythonInside(request);
21+
}
22+
}

demo-app/src/test/java/io/w4t3rcs/demo/TestDemoAppApplication.java

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
package io.w4t3rcs.demo;
22

3+
import io.w4t3rcs.python.PythonGrpcServerContainer;
34
import org.springframework.boot.test.context.TestConfiguration;
5+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
6+
import org.springframework.context.annotation.Bean;
47

58
@TestConfiguration(proxyBeanMethods = false)
69
class TestcontainersConfiguration {
7-
10+
@Bean
11+
@ServiceConnection
12+
PythonGrpcServerContainer pythonGrpcServerContainer() {
13+
return new PythonGrpcServerContainer("w4t3rcs/spring-boot-python-executor-python-grpc-server")
14+
.withUsername("user")
15+
.withPassword("pass")
16+
.withAdditionalImports(new String[]{"scikit-learn", "numpy", "pandas"});
17+
}
818
}

python-server-testcontainers/src/main/java/io/w4t3rcs/python/PythonGrpcServerContainer.java

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,63 @@
66

77
import java.time.Duration;
88

9+
/**
10+
* Container for running a Python gRPC server inside a Docker container.
11+
* <p>
12+
* Extends {@link PythonServerContainer} to provide configuration specific
13+
* to the Python gRPC server implementation used in the Docker image
14+
* {@code w4t3rcs/spring-boot-python-executor-python-grpc-server}.
15+
* <p>
16+
* The container exposes the standard gRPC port {@code 50051} and waits
17+
* for both the port availability and a specific log message indicating
18+
* successful startup.
19+
*
20+
* <pre>{@code
21+
* try (PythonGrpcServerContainer grpcServerContainer = new PythonGrpcServerContainer("w4t3rcs/spring-boot-python-executor-python-grpc-server:latest")
22+
* .withUsername("user")
23+
* .withPassword("pass")) {
24+
* grpcServerContainer.start();
25+
*
26+
* //Do something after the container has been started
27+
* }
28+
* }</pre>
29+
*
30+
* @see PythonServerContainer
31+
* @see PythonRestServerContainer
32+
* @author w4t3rcs
33+
* @since 1.0.0
34+
*/
935
public class PythonGrpcServerContainer extends PythonServerContainer<PythonGrpcServerContainer> {
1036
public static final String PYTHON_SERVER_THREAD_POOL_MAX_WORKERS_ENV = "PYTHON_SERVER_THREAD_POOL_MAX_WORKERS";
1137
private static final DockerImageName DOCKER_IMAGE_NAME = DockerImageName.parse("w4t3rcs/spring-boot-python-executor-python-grpc-server");
1238
private static final int SERVER_DEFAULT_PORT = 50051;
13-
public static final String GRPC_SERVER_RUNNING_MESSAGE = ".*gRPC server running.+";
39+
private static final String GRPC_SERVER_RUNNING_MESSAGE = ".*gRPC server running.+";
1440

41+
/**
42+
* Creates a new {@link PythonGrpcServerContainer} instance with the given Docker image name.
43+
* <p>
44+
* The provided image name must be compatible with
45+
* {@code w4t3rcs/spring-boot-python-executor-python-grpc-server}.
46+
*
47+
* @param image non-null Docker image name string, must be compatible with the expected base image
48+
*/
1549
public PythonGrpcServerContainer(String image) {
1650
this(DockerImageName.parse(image));
1751
}
1852

53+
/**
54+
* Creates a new {@link PythonGrpcServerContainer} instance with the given {@link DockerImageName}.
55+
* <p>
56+
* The provided Docker image must be compatible with
57+
* {@code w4t3rcs/spring-boot-python-executor-python-grpc-server}.
58+
* <p>
59+
* The container exposes port {@value #SERVER_DEFAULT_PORT} and waits
60+
* for the port to be listening as well as the log message defined by
61+
* {@value #GRPC_SERVER_RUNNING_MESSAGE} before considering itself started.
62+
* The startup timeout is 5 minutes.
63+
*
64+
* @param dockerImageName non-null {@link DockerImageName} instance, must be compatible with the expected base image
65+
*/
1966
public PythonGrpcServerContainer(DockerImageName dockerImageName) {
2067
super(dockerImageName);
2168
dockerImageName.assertCompatibleWith(DOCKER_IMAGE_NAME);
@@ -27,12 +74,29 @@ public PythonGrpcServerContainer(DockerImageName dockerImageName) {
2774
.withStrategy(Wait.forLogMessage(GRPC_SERVER_RUNNING_MESSAGE, 1)));
2875
}
2976

77+
/**
78+
* Sets the maximum number of workers for the thread pool in the Python gRPC server.
79+
* <p>
80+
* This value is passed as an environment variable
81+
* {@value #PYTHON_SERVER_THREAD_POOL_MAX_WORKERS_ENV} to the container.
82+
*
83+
* @param maxWorkers non-negative integer specifying maximum thread pool workers, zero or negative values are accepted but their effect depends on server implementation
84+
* @return this container instance for fluent chaining, never {@code null}
85+
*/
3086
public PythonGrpcServerContainer withThreadPoolMaxWorkers(int maxWorkers) {
3187
this.withEnv(PYTHON_SERVER_THREAD_POOL_MAX_WORKERS_ENV, String.valueOf(maxWorkers));
3288
return this.self();
3389
}
3490

91+
/**
92+
* Returns the server URL in the form {@code host:port} where {@code host}
93+
* is the container host and {@code port} is the mapped port {@value #SERVER_DEFAULT_PORT}.
94+
* <p>
95+
* This method should only be called after the container has been started.
96+
*
97+
* @return non-null, non-empty string representing the server URL, e.g. {@code localhost:50051}
98+
*/
3599
public String getServerUrl() {
36100
return this.getHost() + ":" + this.getMappedPort(SERVER_DEFAULT_PORT);
37101
}
38-
}
102+
}

0 commit comments

Comments
 (0)