serious python
Serious Python
Since Flet is built on Flutter, you can embed Flet-created programs inside Flutter to create applications wrapped in Flutter. Internally, this works through an interface between Flutter and Python files packaged with Flet.
flet이 플러터기반으로 만들어진 만큼 flutter내부에 flet으로 만든 프로그램을 심어서 플러터로 감싸서 프로그램을 만들 수 있습니다. 내부적으로는 flutter와 flet으로 패키징된 파이썬 파일과의 인터페이스를 이용해 진행됩니다.
Let’s explore this with actual example code.
그럼 실제적인 예제코드를 가지고 열어보겠습니다.
The example is available at serious-python, which we’ll use for this demonstration.
예제는 serious-python에 있으며 이를 이용해봅니다.
Running Flet-created Programs in Flutter
If you’re not yet familiar with Flutter, let’s learn how to code with Flet and package it with Flutter.
플러터에서 flet으로 만든 프로그램 실행해보기
플러터가 아직 익숙하지 않다면 flet으로 코딩을 하고 플러터로 패키징을 하는 방법을 알아보겠습니다.
After creating a Flutter project, you’ll find a pubspec.yaml
file. This file contains information about your project. Add the following content to the bottom of this file:
Flutter 프로젝트를 생성하시고 나면
pubspec.yaml
파일이 존재합니다. 해당 프로젝트에 대한 정보를 담고있는 파일이며 해당 파일 맨 하단에 아래 내용을 넣습니다.
flutter:
assets:
- app/app.zip
- app/app.zip.hash
This defines the assets to be used in the Flutter project. The app.zip file is defined relative to the root directory where the pubspec.yaml
file is located, meaning we’ll use the app folder and the app.zip file inside it as static files.
플러터 프로젝트에서 사용할 assets를 정의하는데 그 정의되는 app.zip파일은
pubspec.yaml
파일이 있는 root 경고 기준으로 작성되기에 app폴더와 app폴더 안의 app.zip파일을 정적파일로 이용하겠다는 의미입니다.
Now let’s create the app.zip file using Python Flet.
그럼 실제적으로 파이썬 Flet을 이용해 app.zip파일까지 만들어보겠습니다.
Create an app folder in your Flutter project, then create a src folder inside it, and finally create a main.py file.
플러터 프로젝트에 app폴더를 만들어주고 src폴더를 만든 후 main.py파일을 만듭니다.
Let’s use the code we tested previously:
그리고 이전 테스트시 사용했던 코드를 넣도록 하겠습니다.
import flet as ft
def main(page: ft.Page):
page.title = "Flet counter example"
page.vertical_alignment = ft.MainAxisAlignment.CENTER
txt_number = ft.TextField(value="0", text_align=ft.TextAlign.RIGHT, width=100)
def minus_click(e):
txt_number.value = str(int(txt_number.value) - 1)
page.update()
def plus_click(e):
txt_number.value = str(int(txt_number.value) + 1)
page.update()
page.add(
ft.Row(
[
ft.IconButton(ft.icons.REMOVE, on_click=minus_click),
txt_number,
ft.IconButton(ft.icons.ADD, on_click=plus_click),
],
alignment=ft.MainAxisAlignment.CENTER,
)
)
ft.app(target=main)
Since we’ll be running this inside Flutter, we’ve removed view=ft.AppView.WEB_BROWSER
.
이번에는 플러터안에서 실행할것임으로
view=ft.AppView.WEB_BROWSER
는 제거하였습니다.
If you’re having trouble, you can refer to the previous article.
잘안되신다면 이전 글을 참고해보셔도 좋습니다.
blog - python - flet
If the execution is successful, we now need to create an app.zip file from this source. Create a requirements.txt file in the app/src path and add:
flet
실행이 잘되었다면 이제 해당 소스를 app.zip파일로 만들어야합니다. app/src 경로에 requirements.txt파일을 만들고
flet
을 넣고 저장해줍니다.
Now let’s install serious_python by referring to the documentation. The site provides installation instructions and sample examples to help you learn the program, so it’s good to become familiar with the pub dev site.
이제 serious_python를 참고해서 설치를 해봅니다. 해당 사이트를 잘보면 설치하는것과 샘플 예제를 보면서 프로그램을 익힐 수 있으니 pub dev사이트와 친해지면 좋습니다.
The installation command is:
설치 명령어는
flutter pub add serious_python
After installation, the pubspec.yaml
file will have:
설치하고나면
pubspec.yaml
파일에
serious_python: ^0.8.4
You can also directly add the package name and version to this location and reinstall using flutter pub get
. Sometimes, due to conflicts, you may need to reset with flutter pub clean
or reinstall when getting source code through git.
이렇게 패키지가 추가되고 해당 위치에 직접 패키지명과 버전을 넣으셔서
flutter pub get
을 이용해 다시 설치가 가능합니다. 간혹 충돌이 있어서flutter pub clean
으로 초기화하거나 git을 통해 소스만 받을 경우도 다시 설치가 가능합니다.
For a more thorough clean, you can follow these steps:
clean을 좀 더 명확하게 하시고 싶으시면 아래 절차를 따르셔도됩니다.
pubspec.lock 삭제
flutter pub cache clean
flutter pub clean
Now that we’ve installed serious_python, let’s create an app.zip file to use our Python files. Enter the following in the terminal:
erious_python을 설치했으니 파이썬으로 만든 파일을 사용하기위해 app.zip파일로 만들어보겠습니다. 터미널에 아래 내용을 입력합니다.
dart run serious_python:main package app/src -p Windows --requirements -r,app/src/requirements.txt
When completed, you’ll see that the app/app.zip folder has been created as specified in the assets path we set earlier.
완료되면 이전에 설정했던 assets 경로처럼 app/app.zip폴더가 생성됨을 확인할 수 있습니다.
This command used Pyodide for web execution, but you can change it to match the appropriate platform.
이번 명령은 웹에서 실행하기위해 Pyodide를 이용했으나 알맞는 플랫폼에 맞게 변경하셔도됩니다.
Flet is currently working on supporting builds with poetry as well, so we’ll soon be able to use more than just requirements.
지금 poetry관련으로도 빌드될수있도록 flet에서도 진행중이니 requirements외에도 곧 사용할수있을것같습니다.
Now let’s add the official example to the main.dart file in the lib folder, which is the Flutter start file:
자 이제 lib 폴더안의 플러터 시작파일인 main.dart에 공식예제를 넣어보겠습니다.
import 'dart:async';
import 'dart:io';
import 'package:flet/flet.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:serious_python/serious_python.dart';
import 'package:url_strategy/url_strategy.dart';
const bool isProduction = bool.fromEnvironment('dart.vm.product');
const assetPath = "app/app.zip";
const pythonModuleName = "main"; // {{ cookiecutter.python_module_name }}
final hideLoadingPage =
bool.tryParse("{{ cookiecutter.hide_loading_animation }}".toLowerCase()) ??
true;
const outLogFilename = "out.log";
const errorExitCode = 100;
const pythonScript = """
import certifi, os, runpy, socket, sys, traceback
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
os.environ["SSL_CERT_FILE"] = certifi.where()
if os.getenv("FLET_PLATFORM") == "android":
import ssl
def create_default_context(
purpose=ssl.Purpose.SERVER_AUTH, *, cafile=None, capath=None, cadata=None
):
return ssl.create_default_context(
purpose=purpose, cafile=certifi.where(), capath=capath, cadata=cadata
)
ssl._create_default_https_context = create_default_context
out_file = open("$outLogFilename", "w+", buffering=1)
callback_socket_addr = os.environ.get("FLET_PYTHON_CALLBACK_SOCKET_ADDR")
if ":" in callback_socket_addr:
addr, port = callback_socket_addr.split(":")
callback_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
callback_socket.connect((addr, int(port)))
else:
callback_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
callback_socket.connect(callback_socket_addr)
sys.stdout = sys.stderr = out_file
def flet_exit(code=0):
callback_socket.sendall(str(code).encode())
out_file.close()
callback_socket.close()
sys.exit = flet_exit
ex = None
try:
runpy.run_module("{module_name}", run_name="__main__")
except Exception as e:
ex = e
traceback.print_exception(e)
sys.exit(0 if ex is None else $errorExitCode)
""";
// global vars
String pageUrl = "";
String assetsDir = "";
String appDir = "";
Map<String, String> environmentVariables = {};
void main() async {
if (isProduction) {
// ignore: avoid_returning_null_for_void
debugPrint = (String? message, {int? wrapWidth}) => null;
}
runApp(FutureBuilder(
future: prepareApp(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
// OK - start Python program
return kIsWeb
? FletApp(
pageUrl: pageUrl,
assetsDir: assetsDir,
hideLoadingPage: hideLoadingPage,
)
: FutureBuilder(
future: runPythonApp(),
builder:
(BuildContext context, AsyncSnapshot<String?> snapshot) {
if (snapshot.hasData || snapshot.hasError) {
// error or premature finish
return MaterialApp(
home: ErrorScreen(
title: "Error running app",
text: snapshot.data ?? snapshot.error.toString()),
);
} else {
// no result of error
return FletApp(
pageUrl: pageUrl,
assetsDir: assetsDir,
hideLoadingPage: hideLoadingPage,
);
}
});
} else if (snapshot.hasError) {
// error
return MaterialApp(
home: ErrorScreen(
title: "Error starting app",
text: snapshot.error.toString()));
} else {
// loading
return const MaterialApp(home: BlankScreen());
}
}));
}
Future prepareApp() async {
if (kIsWeb) {
// web mode - connect via HTTP
pageUrl = Uri.base.toString();
var routeUrlStrategy = getFletRouteUrlStrategy();
if (routeUrlStrategy == "path") {
setPathUrlStrategy();
}
} else {
await setupDesktop();
// extract app from asset
appDir = await extractAssetZip(assetPath, checkHash: true);
// set current directory to app path
Directory.current = appDir;
assetsDir = path.join(appDir, "assets");
// configure apps DATA and TEMP directories
WidgetsFlutterBinding.ensureInitialized();
var appTempPath = (await path_provider.getApplicationCacheDirectory()).path;
var appDataPath =
(await path_provider.getApplicationDocumentsDirectory()).path;
if (defaultTargetPlatform != TargetPlatform.iOS &&
defaultTargetPlatform != TargetPlatform.android) {
// append app name to the path and create dir
PackageInfo packageInfo = await PackageInfo.fromPlatform();
appDataPath = path.join(appDataPath, "flet", packageInfo.packageName);
if (!await Directory(appDataPath).exists()) {
await Directory(appDataPath).create(recursive: true);
}
}
environmentVariables["FLET_APP_DATA"] = appDataPath;
environmentVariables["FLET_APP_TEMP"] = appTempPath;
environmentVariables["FLET_PLATFORM"] =
defaultTargetPlatform.name.toLowerCase();
if (defaultTargetPlatform == TargetPlatform.windows) {
// use TCP on Windows
var tcpPort = await getUnusedPort();
pageUrl = "tcp://localhost:$tcpPort";
environmentVariables["FLET_SERVER_PORT"] = tcpPort.toString();
} else {
// use UDS on other platforms
pageUrl = "flet.sock";
environmentVariables["FLET_SERVER_UDS_PATH"] = pageUrl;
}
}
return "";
}
Future<String?> runPythonApp() async {
var script = pythonScript.replaceAll('{module_name}', pythonModuleName);
var completer = Completer<String>();
ServerSocket outSocketServer;
String socketAddr = "";
StringBuffer pythonOut = StringBuffer();
if (defaultTargetPlatform == TargetPlatform.windows) {
var tcpAddr = "127.0.0.1";
outSocketServer = await ServerSocket.bind(tcpAddr, 0);
debugPrint(
'Python output TCP Server is listening on port ${outSocketServer.port}');
socketAddr = "$tcpAddr:${outSocketServer.port}";
} else {
socketAddr = "stdout.sock";
outSocketServer = await ServerSocket.bind(
InternetAddress(socketAddr, type: InternetAddressType.unix), 0);
debugPrint('Python output Socket Server is listening on $socketAddr');
}
environmentVariables["FLET_PYTHON_CALLBACK_SOCKET_ADDR"] = socketAddr;
void closeOutServer() async {
outSocketServer.close();
int exitCode = int.tryParse(pythonOut.toString().trim()) ?? 0;
if (exitCode == errorExitCode) {
var out = "";
if (await File(outLogFilename).exists()) {
out = await File(outLogFilename).readAsString();
}
completer.complete(out);
} else {
exit(exitCode);
}
}
outSocketServer.listen((client) {
debugPrint(
'Connection from: ${client.remoteAddress.address}:${client.remotePort}');
client.listen((data) {
var s = String.fromCharCodes(data);
pythonOut.write(s);
}, onError: (error) {
client.close();
closeOutServer();
}, onDone: () {
client.close();
closeOutServer();
});
});
// run python async
SeriousPython.runProgram(path.join(appDir, "$pythonModuleName.pyc"),
script: script, environmentVariables: environmentVariables);
// wait for client connection to close
return completer.future;
}
class ErrorScreen extends StatelessWidget {
final String title;
final String text;
const ErrorScreen({super.key, required this.title, required this.text});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
TextButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Copied to clipboard')),
);
},
icon: const Icon(
Icons.copy,
size: 16,
),
label: const Text("Copy"),
)
],
),
Expanded(
child: SingleChildScrollView(
child: SelectableText(text,
style: Theme.of(context).textTheme.bodySmall),
))
],
),
)),
);
}
}
class BlankScreen extends StatelessWidget {
const BlankScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: SizedBox.shrink(),
);
}
}
Future<int> getUnusedPort() {
return ServerSocket.bind("127.0.0.1", 0).then((socket) {
var port = socket.port;
socket.close();
return port;
});
}
Install additional libraries required by the code.
해당 코드에서 추가적으로 필요한 라이브러리를 설치해줍니다.
flutter pub add flet
flutter pub add package_info_plus
flutter pub add url_strategy
After installation, check the pubspec.yaml
file.
설치 후
pubspec.yaml
파일에서 확인한 후
flet: ^0.25.1
package_info_plus: ^8.1.1
url_strategy: ^0.3.0
When you start by setting the device as a desktop, the program written in flet code starts in Flutter. There is a way to write and build a program using Flet, and there is also a way to use it in Flutter like this. If you can use the necessary skills in the right place for the right situation, your range of choices will expand.
디바이스를 데스크탑으로 설정하여 시작하면 flet코드로 짜여진 프로그램이 Flutter에서 시작됩니다. Flet을 이용하여 프로그램을 짜고 빌드하는 방법도 있고 이런식으로 Flutter안에서 사용하는 방식도 있습니다. 필요한 상황에 필요한 스킬을 적재적소에 사용할수있다면 선택의 폭이 넓어집니다.
Try running it by inserting fastapi in flutter
In fact, this part can be done by creating a separate backend and communicating, but I will proceed so that it can be used when working on AI-related on-device, etc. Of course, you can use Python code or flask as shown in the official example without using FastAPi. Let’s proceed to briefly taste FastAPI, which is the step before learning the backend java springboot.
플러터에서 fastapi를 넣어서 실행해보기
사실 이 부분은 별도로 백엔드 만들어서 통신하면 되나 AI측면에서의 온디바이스등을 고려하여 작업하시는 경우에 사용할수 있도록 진행해보겠습니다. 물론 FastAPi를 이용하지않고 공식예제에 나온것처럼 바로 파이썬 코드를 이용할수도 있고 flask를 이용할 수도 있습니다. 백엔드 java springboot를 배우기 전단계인 FastAPI를 잠시 맛보기위해 진행해보겠습니다.
Create a new flutter project and start by setting up assets in the pubspec.yaml
file.
새로 flutter 프로젝트를 생성하고
pubspec.yaml
파일에 assets 설정부터 해줍니다.
flutter:
assets:
- app/app.zip
- app/app.zip.hash
Copy and paste the contents of the main.dart
file in the official example.
공식예제에 있는
main.dart
파일의 내용을 복사하여 넣어줍니다.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:serious_python/serious_python.dart';
void main() {
startPython();
runApp(const MyApp());
}
void startPython() async {
SeriousPython.run("app/app.zip", environmentVariables: {"a": "1", "b": "2"});
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late TextEditingController _controller;
String? _result;
@override
void initState() {
super.initState();
_controller = TextEditingController();
getServiceResult();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future getServiceResult() async {
while (true) {
try {
var response = await http.get(Uri.parse("http://127.0.0.1:55001"));
setState(() {
_result = response.body;
});
return;
} catch (_) {
await Future.delayed(const Duration(milliseconds: 200));
}
}
}
@override
Widget build(BuildContext context) {
Widget? result;
if (_result != null) {
result = Text(_result!);
} else {
result = const CircularProgressIndicator();
}
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: Scaffold(
appBar: AppBar(
title: const Text('Python REPL'),
),
body: SafeArea(
child: Column(children: [
Expanded(
child: Center(
child: result,
),
),
Container(
padding: const EdgeInsets.all(5),
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter Python code',
),
smartQuotesType: SmartQuotesType.disabled,
smartDashesType: SmartDashesType.disabled,
keyboardType: TextInputType.multiline,
minLines: 1,
maxLines: 10,
enabled: _result != null,
)),
const SizedBox(
width: 8,
),
ElevatedButton(
onPressed: _result != null
? () {
setState(() {
_result = null;
});
http
.post(
Uri.parse(
"http://127.0.0.1:55001/python"),
headers: {
'Content-Type': 'application/json'
},
body: json.encode(
{"command": _controller.text}))
.then((resp) => setState(() {
_controller.text = "";
_result = resp.body;
}));
}
: null,
child: const Text("Run"))
],
),
)
]))),
);
}
}
Since we created a new project, we need to install additional packages from the terminal.
그럼 새로 프로젝트를 만들었기때문에 추가적으로 패키지를 터미널에서 설치해야합니다.
flutter pub add http
flutter pub add serious_python
Likewise, check that it is properly installed in your pubspec.yaml
file.
마찬가지로
pubspec.yaml
파일에 제대로 설치되었는지 확인합니다.
http: ^1.2.2
serious_python: ^0.8.4
This time, create an app folder in the flutter project, create a src folder, and then create a main.py file. Here, unlike the example, we will write fastapi code.
이번에는 플러터 프로젝트에 app폴더를 만들어주고 src폴더를 만든 후 main.py파일을 만듭니다. 여기에는 예제와는 다르게 fastapi 코드를 작성해봅니다.
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
from contextlib import redirect_stdout
from io import StringIO
import uvicorn
port = 55001
print("Trying to run a socket server on:", port)
class PythonRunner:
__globals = {}
__locals = {}
def run(self, code):
f = StringIO()
with redirect_stdout(f):
exec(code, self.__globals, self.__locals)
return f.getvalue()
pr = PythonRunner()
app = FastAPI()
@app.get("/")
def hello_world():
return {"message": 'Enter Python code and tap "Run".'}
class PythonCommand(BaseModel):
command: str
@app.post("/python")
async def run_python(payload: PythonCommand):
try:
result = pr.run(payload.command)
return {"result": result}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=port)
To run this source, you need to have fastapi and uvicorn installed. I’ll assume you have poetry installed in global pip in the previous step.
해당 소스를 실행하려면 fastapi와 uvicorn이 설치되어야합니다. 이전 단계에서 전역 pip에 poetry가 설치되어있다고 가정하겠습니다.
blog - python - poetry Reference
> blog - python - poetry 참고
Go to app/src path
> app/src 경로로 이동
poetry init
poetry shell
poetry add fastapi uvicorn
python main.py
If everything went normally, the following message will appear:
정상적으로 진행되었다면 아래와 같은 내용이 뜹니다
Trying to run a socket server on: 55001
INFO: Started server process [19116]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:55001 (Press CTRL+C to quit)
Once normal execution is confirmed, exit using CTRL+C
and create a requirements.txt file and write the contents below.
정상실행이 확인되었으면
CTRL+C
를 이용하여 종료하고 requirements.txt 파일을 만들고 아래 내용을 작성해줍니다.
fastapi<0.100
uvicorn
Now let’s create an app.zip file. Go to the project root path and type the following into the terminal.
자 이제 app.zip파일을 만들어보겠습니다. 프로젝트 root 경로로 이동하여 터미널에 아래 내용을 입력합니다.
dart run serious_python:main package app/src -p Windows --requirements -r,app/src/requirements.txt
Now go back to the main.dart file and run it. In the python code input box below,
이제 main.dart 파일로 돌아와서 실행합니다. 하단의 python code 입력칸에
print('test')
Enter and press the run button.
을 입력하고 run 버튼을 누르면
{"result":"test\n"}
I get a response like this:
이라는 응답을 받습니다.
We have briefly looked into using fastapi like this. Even if it is not fastapi, you can use it as much as you know how to use python in flutter.
이렇게 fastapi를 사용하는것에 대해 잠시 알아보았습니다. 꼭 fastapi가 아니더라도 flutter에서 python을 어떻게 사용할지는 아는만큼 사용할수있습니다.
Related Pages
- 1. DartPAD
- 2. GitHubGist
- 3. Setting
- 4. Flutter
- 5. SeriousPython