Skip to content

Commit

Permalink
added graalpy (#1473)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasstupka committed Sep 17, 2024
1 parent 15ce207 commit 8ce1d83
Show file tree
Hide file tree
Showing 21 changed files with 560 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.micronaut.guides.feature;

import io.micronaut.guides.feature.template.graalpyMavenPluginPackage;
import io.micronaut.starter.application.generator.GeneratorContext;
import io.micronaut.starter.build.BuildProperties;
import io.micronaut.starter.build.dependencies.CoordinateResolver;
import io.micronaut.starter.build.maven.MavenPlugin;
import io.micronaut.starter.options.BuildTool;
import io.micronaut.starter.template.RockerWritable;
import jakarta.inject.Singleton;

import java.util.Collections;
import java.util.List;

@Singleton
class MicronautGraalpy extends AbstractFeature {

private static final String GROUP_ID_GRAALVM_PYTHON = "org.graalvm.python";
private static final String ARTIFACT_ID_GRAALPY_MAVEN_PLUGIN = "graalpy-maven-plugin";

private final CoordinateResolver coordinateResolver;

MicronautGraalpy(CoordinateResolver coordinateResolver) {
super("graalpy", "micronaut-graalpy");
this.coordinateResolver = coordinateResolver;
}

@Override
public void apply(GeneratorContext generatorContext) {
super.apply(generatorContext);
if (generatorContext.getBuildTool() == BuildTool.MAVEN) {
BuildProperties props = generatorContext.getBuildProperties();
coordinateResolver.resolve(ARTIFACT_ID_GRAALPY_MAVEN_PLUGIN)
.ifPresent(coordinate -> props.put("graalpy.maven.plugin.version", coordinate.getVersion()));
generatorContext.addBuildPlugin(graalpyMavenPlugin());
}
}

protected MavenPlugin graalpyMavenPlugin() {
return MavenPlugin.builder()
.groupId(GROUP_ID_GRAALVM_PYTHON)
.artifactId(ARTIFACT_ID_GRAALPY_MAVEN_PLUGIN)
.extension(new RockerWritable(graalpyMavenPluginPackage.template(pythonPackages())))
.build();
}

protected List<String> pythonPackages() {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.micronaut.guides.feature;

import io.micronaut.starter.build.dependencies.CoordinateResolver;
import jakarta.inject.Singleton;

import java.util.Collections;
import java.util.List;

@Singleton
class MicronautGraalpyPygal extends MicronautGraalpy {

MicronautGraalpyPygal(CoordinateResolver coordinateResolver) {
super(coordinateResolver);
}

@Override
public String getName() {
return "graalpy-pygal";
}

@Override
protected List<String> pythonPackages() {
return Collections.singletonList("pygal==3.0.4");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@import java.util.List
@args (List<String> packages)
<!-- tag::graalpy-maven-plugin[] -->
<plugin>
<groupId>org.graalvm.python</groupId>
<artifactId>graalpy-maven-plugin</artifactId>
<version>${graalpy.maven.plugin.version}</version>
@if(!packages.isEmpty()) {
<configuration>
<packages>
@for (String pkg : packages) {
<package>@(pkg)</package>
}
</packages>
</configuration>
}
<executions>
<execution>
<goals>
<goal>process-graalpy-resources</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- end::graalpy-maven-plugin[] -->
10 changes: 10 additions & 0 deletions buildSrc/src/main/resources/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.graalvm.python</groupId>
<artifactId>graalpy-maven-plugin</artifactId>
<version>24.1.0</version>
</dependency>
<dependency>
<groupId>io.micronaut.graal-languages</groupId>
<artifactId>micronaut-graalpy</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.github.librepdf</groupId>
<artifactId>openpdf</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.micronaut;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;

@Controller("/pygal") // <1>
class PygalController {

private final PygalModule pygal;

PygalController(PygalModule pygal) { // <2>
this.pygal = pygal;
}

@ExecuteOn(TaskExecutors.BLOCKING)
@Get // <3>
@Produces("image/svg+xml") // <4>
public String index() {
PygalModule.StackedBar stackedBar = pygal.StackedBar(); // <5>
stackedBar.add("Fibonacci", new int[] {0, 1, 1, 2, 3, 5, 8}); // <6>
PygalModule.Svg svg = stackedBar.render(); // <7>
return svg.decode(); // <8>
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package example.micronaut;

import io.micronaut.graal.graalpy.annotations.GraalPyModule;

@GraalPyModule("pygal") // <1>
interface PygalModule {
StackedBar StackedBar(); // <2>

interface StackedBar { // <3>
void add(String title, int[] i); // <4>
Svg render(); // <5>
}

interface Svg { // <6>
String decode(); // <7>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
["example.micronaut.PygalModule"],
["example.micronaut.PygalModule$StackedBar"],
["example.micronaut.PygalModule$Svg"]
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.micronaut;

import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.runtime.server.EmbeddedServer;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

@MicronautTest // <1>
class PygalControllerTest {

@Test
void testPygalResponse(@Client("/") HttpClient client) { // <2>
String response = client.toBlocking().retrieve(HttpRequest.GET("/pygal")); // <3>
assertTrue(response.contains("<svg xmlns:xlink"));
assertTrue(response.contains("<title>Pygal</title>"));
assertTrue(response.contains("<g class=\"graph stackedbar-graph vertical\">"));
}
}
18 changes: 18 additions & 0 deletions guides/micronaut-graalpy-python-package/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"title": "Creating a Micronaut GraalPy application using a Python package",
"intro": "Learn how to create a simple Micronaut GraalPy application using a third party Python package.",
"authors": ["Tomas Stupka"],
"tags": ["junit"],
"languages": ["java"],
"buildTools": ["maven"],
"categories": ["Getting Started"],
"minimumJavaVersion": 21,
"publicationDate": "2024-07-01",
"apps": [
{
"name": "default",
"invisibleFeatures": ["graalpy-pygal"],
"features": ["graalpy"]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
common:header.adoc[]

common:requirements.adoc[]

common:completesolution.adoc[]

common:create-app-features.adoc[]

=== GraalPy Maven Plugin
Insert a `<packages>` element into the GraalPy Maven Plugin `<configuration>` element, specifying the Python packages to download and install as shown below:


resource:../../../pom.xml[tag=graalpy-maven-plugin]

Adding `<package>pygal==3.0.4</package>` instructs the plugin to download and install the Python https://www.pygal.org/en/stable/[Pygal] package at build time.
The package is included in the Java resources and becomes available to GraalPy.

=== Python code
We are going to use the https://www.pygal.org/en/stable/documentation/types/bar.html#stacked[StackedBar] class from https://www.pygal.org/en/stable/[Pygal] package to render a graph
in https://www.pygal.org/en/3.0.0/documentation/output.html#svg[Svg] format.

=== Java binding
Python code can be accessed programmatically using the https://www.graalvm.org/sdk/javadoc/org/graalvm/polyglot/package-summary.html[GraalVM SDK Polyglot API],
which enables you to embed Python into your applications.

In order to make it work, we first need a Java interface providing the intended binding to Python.

Create a Java interface with the following code:
source:PygalModule[]
<1> The @api@/io/micronaut/graal/graalpy/annotations/GraalPyModule.html[@GraalPyModule] annotation indicates that the bean created from the interface
is intended to import the `pygal` Python package and expose it to the Java code using the https://www.graalvm.org/truffle/javadoc/org/graalvm/polyglot/Value.html#target-type-mapping-heading[Target type mapping].
<2> Java method matching the `StackedBar` Python class constructor.
<3> Java interface with methods matching functions from the `StackedBar` Python class we want to use.
<4> Java method matching the `StackedBar.add(title, values)` Python function.
<5> Java method matching the `StackedBar.render()` Python function.
<6> Java interface with methods matching functions from the `Svg` Python class we want to use.
<7> Java method matching the `Svg.decode()` Python function.

=== Controller
To create a microservice that provides the graph rendered by `Pygal` you also need a controller.

Create a controller:
source:PygalController[]

callout:controller[number=1,arg0=/pygal]
callout:constructor-di[number=2,arg0=PygalModule]
callout:get[number=3,arg0=index,arg1=/pygal]
<5> By default, a Micronaut response uses `application/json` as Content-Type. We are returning `svg/xml`, so we have to set it.
<6> Use the `PygalModule` bean to create a `PygalModule.StackedBar` instance.
<7> Add graph title and data.
<8> Render the graph.
<9> Return the graphs `svg/xml` representation decoded as a string.

=== Test

Create a test to verify that when you make a GET request to `/pygal` you get the `svg/xml` response:

test:PygalControllerTest[]

callout:micronaut-test[1]
callout:http-client[2]
callout:http-request[3]

common:testApp.adoc[]
common:runapp.adoc[]

Open `http://localhost:8080/pygal` in a web browser of your choice execute the endpoint. You should see a stacked bar chart.

image::graalpy-pygal.png[]

:exclude-for-languages:groovy

== GraalVM Native Executable

:leveloffset: +1

=== Native Executable metadata
The https://www.graalvm.org/[GraalVM] Native Image compilation requires metadata to properly run code that uses https://www.graalvm.org/latest/reference-manual/native-image/metadata/#dynamic-proxy[dynamic proxies].

For the case that also a native executable has to be generated, create a proxy configuration file:
resource:META-INF/native-image/proxy-config.json[]

common:graal-with-plugins.adoc[]

Open `http://localhost:8080/pygal` in a web browser of your choice to execute the endpoint exposed by the native executable. You should see a stacked bar chart.

common:nativetest.adoc[]

:exclude-for-languages:

:leveloffset: -1

== Next steps

Read more about https://micronaut-projects.github.io/micronaut-test/latest/guide/[Micronaut testing].

common:helpWithMicronaut.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.micronaut;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;

@Controller("/hello") // <1>
class HelloController {

private final HelloModule hello;

HelloController(HelloModule hello) { // <2>
this.hello = hello;
}

@Get // <3>
@Produces(MediaType.TEXT_PLAIN) // <4>
String index() {
return hello.hello("World"); // <5>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package example.micronaut;

import io.micronaut.graal.graalpy.annotations.GraalPyModule;

@GraalPyModule("hello") // <1>
public interface HelloModule {
String hello(String txt); // <2>
}
Loading

0 comments on commit 8ce1d83

Please sign in to comment.