feat: Initial flatpak support

This commit is contained in:
Mr Finchum 2025-01-30 14:29:45 +00:00
parent d608156206
commit 51108ef86e
13 changed files with 771 additions and 1 deletions

2
.gitignore vendored
View file

@ -2,3 +2,5 @@ test/
dist/
.ropeproject/
__pycache__/
.flatpak-builder/
flatpak-build-dir/

View file

@ -1,5 +1,21 @@
# Changelog
## 0.6.x
### 0.6.0: Initial Flatpak Support
- Started Flatpak package building.
- Not added to Flathub yet, as only stable software is hosted there.
- Not fully completed, icon, name, and description are included, but the version is missing for some reason.
- Local build and installation work. The Bash script `build_flatpak.sh` in the `flatpak/` directory generates all pip dependencies, then builds and installs the app locally.
- `requirements-parser` has to be installed from pip to finish installing the flatpak (maybe more pypi packages..)
---
## 0.5.x
### 0.5.0
- Removed all leftover of tui code that was hiding in some classes.
---
## 0.4.x
### 0.4.0
- Fixed a critical issue that prevented the program from functioning.

BIN
flatpak/app-icon.xcf Normal file

Binary file not shown.

5
flatpak/build_flatpak.sh Executable file
View file

@ -0,0 +1,5 @@
#!/bin/bash
# runtime, skd, and base has to be installed, see net.boxyfoxy.net.OptimaLab35.json
# uses [flatpak-pip-generator](https://github.com/flatpak/flatpak-builder-tools/tree/master/pip) to download and build all dependency from pip
python flatpak-pip-generator --runtime='org.kde.Sdk//6.8' piexif pillow optima35 PyYAML hatchling
flatpak-builder --user --install flatpak-build-dir net.boxyfoxy.OptimaLab35.json --force-clean

3
flatpak/flathub.json Normal file
View file

@ -0,0 +1,3 @@
{
"only-arches": ["x86_64"]
}

533
flatpak/flatpak-pip-generator Executable file
View file

@ -0,0 +1,533 @@
#!/usr/bin/env python3
__license__ = 'MIT'
import argparse
import json
import hashlib
import os
import re
import shutil
import subprocess
import sys
import tempfile
import urllib.request
from collections import OrderedDict
from typing import Dict
try:
import requirements
except ImportError:
exit('Requirements modules is not installed. Run "pip install requirements-parser"')
parser = argparse.ArgumentParser()
parser.add_argument('packages', nargs='*')
parser.add_argument('--python2', action='store_true',
help='Look for a Python 2 package')
parser.add_argument('--cleanup', choices=['scripts', 'all'],
help='Select what to clean up after build')
parser.add_argument('--requirements-file', '-r',
help='Specify requirements.txt file')
parser.add_argument('--build-only', action='store_const',
dest='cleanup', const='all',
help='Clean up all files after build')
parser.add_argument('--build-isolation', action='store_true',
default=False,
help=(
'Do not disable build isolation. '
'Mostly useful on pip that does\'t '
'support the feature.'
))
parser.add_argument('--ignore-installed',
type=lambda s: s.split(','),
default='',
help='Comma-separated list of package names for which pip '
'should ignore already installed packages. Useful when '
'the package is installed in the SDK but not in the '
'runtime.')
parser.add_argument('--checker-data', action='store_true',
help='Include x-checker-data in output for the "Flatpak External Data Checker"')
parser.add_argument('--output', '-o',
help='Specify output file name')
parser.add_argument('--runtime',
help='Specify a flatpak to run pip inside of a sandbox, ensures python version compatibility')
parser.add_argument('--yaml', action='store_true',
help='Use YAML as output format instead of JSON')
parser.add_argument('--ignore-errors', action='store_true',
help='Ignore errors when downloading packages')
parser.add_argument('--ignore-pkg', nargs='*',
help='Ignore a package when generating the manifest. Can only be used with a requirements file')
opts = parser.parse_args()
if opts.yaml:
try:
import yaml
except ImportError:
exit('PyYAML modules is not installed. Run "pip install PyYAML"')
def get_pypi_url(name: str, filename: str) -> str:
url = 'https://pypi.org/pypi/{}/json'.format(name)
print('Extracting download url for', name)
with urllib.request.urlopen(url) as response:
body = json.loads(response.read().decode('utf-8'))
for release in body['releases'].values():
for source in release:
if source['filename'] == filename:
return source['url']
raise Exception('Failed to extract url from {}'.format(url))
def get_tar_package_url_pypi(name: str, version: str) -> str:
url = 'https://pypi.org/pypi/{}/{}/json'.format(name, version)
with urllib.request.urlopen(url) as response:
body = json.loads(response.read().decode('utf-8'))
for ext in ['bz2', 'gz', 'xz', 'zip', 'none-any.whl']:
for source in body['urls']:
if source['url'].endswith(ext):
return source['url']
err = 'Failed to get {}-{} source from {}'.format(name, version, url)
raise Exception(err)
def get_package_name(filename: str) -> str:
if filename.endswith(('bz2', 'gz', 'xz', 'zip')):
segments = filename.split('-')
if len(segments) == 2:
return segments[0]
return '-'.join(segments[:len(segments) - 1])
elif filename.endswith('whl'):
segments = filename.split('-')
if len(segments) == 5:
return segments[0]
candidate = segments[:len(segments) - 4]
# Some packages list the version number twice
# e.g. PyQt5-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux2014_x86_64.whl
if candidate[-1] == segments[len(segments) - 4]:
return '-'.join(candidate[:-1])
return '-'.join(candidate)
else:
raise Exception(
'Downloaded filename: {} does not end with bz2, gz, xz, zip, or whl'.format(filename)
)
def get_file_version(filename: str) -> str:
name = get_package_name(filename)
segments = filename.split(name + '-')
version = segments[1].split('-')[0]
for ext in ['tar.gz', 'whl', 'tar.xz', 'tar.gz', 'tar.bz2', 'zip']:
version = version.replace('.' + ext, '')
return version
def get_file_hash(filename: str) -> str:
sha = hashlib.sha256()
print('Generating hash for', filename.split('/')[-1])
with open(filename, 'rb') as f:
while True:
data = f.read(1024 * 1024 * 32)
if not data:
break
sha.update(data)
return sha.hexdigest()
def download_tar_pypi(url: str, tempdir: str) -> None:
with urllib.request.urlopen(url) as response:
file_path = os.path.join(tempdir, url.split('/')[-1])
with open(file_path, 'x+b') as tar_file:
shutil.copyfileobj(response, tar_file)
def parse_continuation_lines(fin):
for line in fin:
line = line.rstrip('\n')
while line.endswith('\\'):
try:
line = line[:-1] + next(fin).rstrip('\n')
except StopIteration:
exit('Requirements have a wrong number of line continuation characters "\\"')
yield line
def fprint(string: str) -> None:
separator = '=' * 72 # Same as `flatpak-builder`
print(separator)
print(string)
print(separator)
packages = []
if opts.requirements_file:
requirements_file_input = os.path.expanduser(opts.requirements_file)
try:
with open(requirements_file_input, 'r') as req_file:
reqs = parse_continuation_lines(req_file)
reqs_as_str = '\n'.join([r.split('--hash')[0] for r in reqs])
reqs_list_raw = reqs_as_str.splitlines()
py_version_regex = re.compile(r';.*python_version .+$') # Remove when pip-generator can handle python_version
reqs_list = [py_version_regex.sub('', p) for p in reqs_list_raw]
if opts.ignore_pkg:
reqs_new = '\n'.join(i for i in reqs_list if i not in opts.ignore_pkg)
else:
reqs_new = reqs_as_str
packages = list(requirements.parse(reqs_new))
with tempfile.NamedTemporaryFile('w', delete=False, prefix='requirements.') as req_file:
req_file.write(reqs_new)
requirements_file_output = req_file.name
except FileNotFoundError as err:
print(err)
sys.exit(1)
elif opts.packages:
packages = list(requirements.parse('\n'.join(opts.packages)))
with tempfile.NamedTemporaryFile('w', delete=False, prefix='requirements.') as req_file:
req_file.write('\n'.join(opts.packages))
requirements_file_output = req_file.name
else:
if not len(sys.argv) > 1:
exit('Please specifiy either packages or requirements file argument')
else:
exit('This option can only be used with requirements file')
for i in packages:
if i["name"].lower().startswith("pyqt"):
print("PyQt packages are not supported by flapak-pip-generator")
print("However, there is a BaseApp for PyQt available, that you should use")
print("Visit https://github.com/flathub/com.riverbankcomputing.PyQt.BaseApp for more information")
sys.exit(0)
with open(requirements_file_output, 'r') as req_file:
use_hash = '--hash=' in req_file.read()
python_version = '2' if opts.python2 else '3'
if opts.python2:
pip_executable = 'pip2'
else:
pip_executable = 'pip3'
if opts.runtime:
flatpak_cmd = [
'flatpak',
'--devel',
'--share=network',
'--filesystem=/tmp',
'--command={}'.format(pip_executable),
'run',
opts.runtime
]
if opts.requirements_file:
if os.path.exists(requirements_file_output):
prefix = os.path.realpath(requirements_file_output)
flag = '--filesystem={}'.format(prefix)
flatpak_cmd.insert(1,flag)
else:
flatpak_cmd = [pip_executable]
output_path = ''
if opts.output:
output_path = os.path.dirname(opts.output)
output_package = os.path.basename(opts.output)
elif opts.requirements_file:
output_package = 'python{}-{}'.format(
python_version,
os.path.basename(opts.requirements_file).replace('.txt', ''),
)
elif len(packages) == 1:
output_package = 'python{}-{}'.format(
python_version, packages[0].name,
)
else:
output_package = 'python{}-modules'.format(python_version)
if opts.yaml:
output_filename = os.path.join(output_path, output_package) + '.yaml'
else:
output_filename = os.path.join(output_path, output_package) + '.json'
modules = []
vcs_modules = []
sources = {}
unresolved_dependencies_errors = []
tempdir_prefix = 'pip-generator-{}'.format(output_package)
with tempfile.TemporaryDirectory(prefix=tempdir_prefix) as tempdir:
pip_download = flatpak_cmd + [
'download',
'--exists-action=i',
'--dest',
tempdir,
'-r',
requirements_file_output
]
if use_hash:
pip_download.append('--require-hashes')
fprint('Downloading sources')
cmd = ' '.join(pip_download)
print('Running: "{}"'.format(cmd))
try:
subprocess.run(pip_download, check=True)
os.remove(requirements_file_output)
except subprocess.CalledProcessError:
os.remove(requirements_file_output)
print('Failed to download')
print('Please fix the module manually in the generated file')
if not opts.ignore_errors:
print('Ignore the error by passing --ignore-errors')
raise
try:
os.remove(requirements_file_output)
except FileNotFoundError:
pass
fprint('Downloading arch independent packages')
for filename in os.listdir(tempdir):
if not filename.endswith(('bz2', 'any.whl', 'gz', 'xz', 'zip')):
version = get_file_version(filename)
name = get_package_name(filename)
try:
url = get_tar_package_url_pypi(name, version)
print('Downloading {}'.format(url))
download_tar_pypi(url, tempdir)
except Exception as err:
# Can happen if only an arch dependent wheel is available like for wasmtime-27.0.2
unresolved_dependencies_errors.append(err)
print('Deleting', filename)
try:
os.remove(os.path.join(tempdir, filename))
except FileNotFoundError:
pass
files = {get_package_name(f): [] for f in os.listdir(tempdir)}
for filename in os.listdir(tempdir):
name = get_package_name(filename)
files[name].append(filename)
# Delete redundant sources, for vcs sources
for name in files:
if len(files[name]) > 1:
zip_source = False
for f in files[name]:
if f.endswith('.zip'):
zip_source = True
if zip_source:
for f in files[name]:
if not f.endswith('.zip'):
try:
os.remove(os.path.join(tempdir, f))
except FileNotFoundError:
pass
vcs_packages = {
x.name: {'vcs': x.vcs, 'revision': x.revision, 'uri': x.uri}
for x in packages
if x.vcs
}
fprint('Obtaining hashes and urls')
for filename in os.listdir(tempdir):
name = get_package_name(filename)
sha256 = get_file_hash(os.path.join(tempdir, filename))
is_pypi = False
if name in vcs_packages:
uri = vcs_packages[name]['uri']
revision = vcs_packages[name]['revision']
vcs = vcs_packages[name]['vcs']
url = 'https://' + uri.split('://', 1)[1]
s = 'commit'
if vcs == 'svn':
s = 'revision'
source = OrderedDict([
('type', vcs),
('url', url),
(s, revision),
])
is_vcs = True
else:
name = name.casefold()
is_pypi = True
url = get_pypi_url(name, filename)
source = OrderedDict([
('type', 'file'),
('url', url),
('sha256', sha256)])
if opts.checker_data:
source['x-checker-data'] = {
'type': 'pypi',
'name': name}
if url.endswith(".whl"):
source['x-checker-data']['packagetype'] = 'bdist_wheel'
is_vcs = False
sources[name] = {'source': source, 'vcs': is_vcs, 'pypi': is_pypi}
# Python3 packages that come as part of org.freedesktop.Sdk.
system_packages = ['cython', 'easy_install', 'mako', 'markdown', 'meson', 'pip', 'pygments', 'setuptools', 'six', 'wheel']
fprint('Generating dependencies')
for package in packages:
if package.name is None:
print('Warning: skipping invalid requirement specification {} because it is missing a name'.format(package.line), file=sys.stderr)
print('Append #egg=<pkgname> to the end of the requirement line to fix', file=sys.stderr)
continue
elif package.name.casefold() in system_packages:
print(f"{package.name} is in system_packages. Skipping.")
continue
if len(package.extras) > 0:
extras = '[' + ','.join(extra for extra in package.extras) + ']'
else:
extras = ''
version_list = [x[0] + x[1] for x in package.specs]
version = ','.join(version_list)
if package.vcs:
revision = ''
if package.revision:
revision = '@' + package.revision
pkg = package.uri + revision + '#egg=' + package.name
else:
pkg = package.name + extras + version
dependencies = []
# Downloads the package again to list dependencies
tempdir_prefix = 'pip-generator-{}'.format(package.name)
with tempfile.TemporaryDirectory(prefix='{}-{}'.format(tempdir_prefix, package.name)) as tempdir:
pip_download = flatpak_cmd + [
'download',
'--exists-action=i',
'--dest',
tempdir,
]
try:
print('Generating dependencies for {}'.format(package.name))
subprocess.run(pip_download + [pkg], check=True, stdout=subprocess.DEVNULL)
for filename in sorted(os.listdir(tempdir)):
dep_name = get_package_name(filename)
if dep_name.casefold() in system_packages:
continue
dependencies.append(dep_name)
except subprocess.CalledProcessError:
print('Failed to download {}'.format(package.name))
is_vcs = True if package.vcs else False
package_sources = []
for dependency in dependencies:
casefolded = dependency.casefold()
if casefolded in sources and sources[casefolded].get("pypi") is True:
source = sources[casefolded]
elif dependency in sources and sources[dependency].get("pypi") is False:
source = sources[dependency]
elif (
casefolded.replace("_", "-") in sources
and sources[casefolded.replace("_", "-")].get("pypi") is True
):
source = sources[casefolded.replace("_", "-")]
elif (
dependency.replace("_", "-") in sources
and sources[dependency.replace("_", "-")].get("pypi") is False
):
source = sources[dependency.replace("_", "-")]
else:
continue
if not (not source['vcs'] or is_vcs):
continue
package_sources.append(source['source'])
if package.vcs:
name_for_pip = '.'
else:
name_for_pip = pkg
module_name = 'python{}-{}'.format(python_version, package.name)
pip_command = [
pip_executable,
'install',
'--verbose',
'--exists-action=i',
'--no-index',
'--find-links="file://${PWD}"',
'--prefix=${FLATPAK_DEST}',
'"{}"'.format(name_for_pip)
]
if package.name in opts.ignore_installed:
pip_command.append('--ignore-installed')
if not opts.build_isolation:
pip_command.append('--no-build-isolation')
module = OrderedDict([
('name', module_name),
('buildsystem', 'simple'),
('build-commands', [' '.join(pip_command)]),
('sources', package_sources),
])
if opts.cleanup == 'all':
module['cleanup'] = ['*']
elif opts.cleanup == 'scripts':
module['cleanup'] = ['/bin', '/share/man/man1']
if package.vcs:
vcs_modules.append(module)
else:
modules.append(module)
modules = vcs_modules + modules
if len(modules) == 1:
pypi_module = modules[0]
else:
pypi_module = {
'name': output_package,
'buildsystem': 'simple',
'build-commands': [],
'modules': modules,
}
print()
with open(output_filename, 'w') as output:
if opts.yaml:
class OrderedDumper(yaml.Dumper):
def increase_indent(self, flow=False, indentless=False):
return super(OrderedDumper, self).increase_indent(flow, False)
def dict_representer(dumper, data):
return dumper.represent_dict(data.items())
OrderedDumper.add_representer(OrderedDict, dict_representer)
output.write("# Generated with flatpak-pip-generator " + " ".join(sys.argv[1:]) + "\n")
yaml.dump(pypi_module, output, Dumper=OrderedDumper)
else:
output.write(json.dumps(pypi_module, indent=4))
print('Output saved to {}'.format(output_filename))
if len(unresolved_dependencies_errors) != 0:
print("Unresolved dependencies. Handle them manually")
for e in unresolved_dependencies_errors:
print(f"- ERROR: {e}")
workaround = """Example how to handle wheels which only support specific architectures:
- type: file
url: https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
sha256: 7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e
only-arches:
- aarch64
- type: file
url: https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
sha256: 666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5
only-arches:
- x86_64
"""
raise Exception(f"Not all dependencies can be determined. Handle them manually.\n{workaround}")

View file

@ -0,0 +1,32 @@
{
"id": "net.boxyfoxy.OptimaLab35",
"runtime": "org.kde.Platform",
"runtime-version": "6.8",
"sdk": "org.kde.Sdk",
"sdk-version": "6.8",
"base": "io.qt.PySide.BaseApp",
"base-version": "6.8",
"command": "OptimaLab35",
"version": "1.0",
"finish-args": ["--socket=wayland", "--socket=x11"],
"modules": [
"python3-modules.json",
{
"name": "OptimaLab35",
"buildsystem": "simple",
"build-commands": [
"pip install --no-build-isolation --prefix=/app .",
"ls",
"install -D flatpak/net.boxyfoxy.OptimaLab35.desktop /app/share/applications/net.boxyfoxy.OptimaLab35.desktop",
"install -D flatpak/net.boxyfoxy.OptimaLab35.metainfo.xml /app/share/metainfo/net.boxyfoxy.OptimaLab35.metainfo.xml",
"install -D flatpak/app-icon.png /app/share/icons/hicolor/512x512/apps/net.boxyfoxy.OptimaLab35.png"
],
"sources": [
{
"type": "dir",
"path": "../src"
}
]
}
]
}

View file

@ -0,0 +1,107 @@
{
"name": "python3-modules",
"buildsystem": "simple",
"build-commands": [],
"modules": [
{
"name": "python3-piexif",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"piexif\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/2c/d8/6f63147dd73373d051c5eb049ecd841207f898f50a5a1d4378594178f6cf/piexif-1.1.3-py2.py3-none-any.whl",
"sha256": "3bc435d171720150b81b15d27e05e54b8abbde7b4242cddd81ef160d283108b6"
}
]
},
{
"name": "python3-pillow",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pillow\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz",
"sha256": "368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"
}
]
},
{
"name": "python3-optima35",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"optima35\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/da/44/f304f2f1f333204dcf57cb883c15acf32aaecdca13730b9822dce79a1b3e/optima35-1.0.1-py3-none-any.whl",
"sha256": "76f5623c4d6bfa57230c9d485a16f6336e91fc8a0a6ad88d42618b5193c9f587"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/2c/d8/6f63147dd73373d051c5eb049ecd841207f898f50a5a1d4378594178f6cf/piexif-1.1.3-py2.py3-none-any.whl",
"sha256": "3bc435d171720150b81b15d27e05e54b8abbde7b4242cddd81ef160d283108b6"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz",
"sha256": "368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"
}
]
},
{
"name": "python3-PyYAML",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"PyYAML\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz",
"sha256": "d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"
}
]
},
{
"name": "python3-hatchling",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"hatchling\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl",
"sha256": "d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl",
"sha256": "09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl",
"sha256": "a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl",
"sha256": "44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/2b/c5/6422dbc59954389b20b2aba85b737ab4a552e357e7ea14b52f40312e7c84/trove_classifiers-2025.1.15.22-py3-none-any.whl",
"sha256": "5f19c789d4f17f501d36c94dbbf969fb3e8c2784d008e6f5164dd2c3d6a2b07c"
}
]
}
]
}

View file

@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "OptimaLab35"
dynamic = ["version"]
authors = [{ name = "Mr. Finchum" }]
authors = [{ name = "Mr Finchum" }]
description = "User interface for optima35."
readme = "pip_README.md"
requires-python = ">=3.8"

BIN
src/flatpak/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -0,0 +1,11 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=OptimaLab35
Comment=A simple tool for editing images and managing metadata, designed for streamlined organization and EXIF adjustments.
Categories=Graphics;Photography;
Icon=net.boxyfoxy.OptimaLab35
Exec=OptimaLab35
Terminal=false

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>net.boxyfoxy.OptimaLab35</id>
<name>OptimaLab35</name>
<summary>A simple tool for editing images and managing metadata, designed for streamlined organization and EXIF adjustments.</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>AGPL-3.0-only</project_license>
<version>1.0.0</version>
<release>
<version>1.0.0</version>
<date>2024-01-30</date>
</release>
<description>
<p>
OptimaLab35 is a image editing and metadata management tool, designed with analog photography in mind. It provides an intuitive way to modify and add EXIF data, making scanned film images easier to organize. With features tailored for efficient batch processing, it helps photographers maintain a structured archive while preserving key details about their images.
</p>
</description>
<launchable type="desktop-id">net.boxyfoxy.OptimaLab35.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/main_tab.png</image>
</screenshot>
<screenshot>
<image>https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/exif_tab.png</image>
</screenshot>
<screenshot>
<image>https://gitlab.com/CodeByMrFinchum/OptimaLab35/-/raw/main/media/preview_window.png</image>
</screenshot>
</screenshots>
</component>

28
src/pyproject.toml Normal file
View file

@ -0,0 +1,28 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "OptimaLab35"
dynamic = ["version"]
authors = [{ name = "Mr Finchum" }]
description = "User interface for optima35."
requires-python = ">=3.8"
dependencies = ["optima35>=1.0.0, <2.0.0", "PyYAML"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
"Operating System :: OS Independent",
]
[project.urls]
Source = "https://gitlab.com/CodeByMrFinchum/OptimaLab35"
[project.scripts]
OptimaLab35 = "OptimaLab35.__main__:main"
[tool.hatch.build.targets.wheel]
packages = ["OptimaLab35"]
[tool.hatch.version]
path = "OptimaLab35/__init__.py"