mvp implementation

This commit is contained in:
2026-03-26 14:01:10 -07:00
parent 0cde587220
commit ebcf404fde
33 changed files with 3048 additions and 6 deletions
+9
View File
@@ -0,0 +1,9 @@
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
MINIO_ENDPOINT=minio:9000
MINIO_PUBLIC_ENDPOINT=localhost:9000
MINIO_USE_SSL=false
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_SESSION_TTL_SECONDS=3600
CONVERSION_CLEANUP_DELAY_SECONDS=3600
+22
View File
@@ -0,0 +1,22 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app/gen/python:/app/python/packages/officeconvert/src:/app/python/packages/server/src
RUN apt-get update && apt-get install -y --no-install-recommends \
libreoffice \
poppler-utils \
fonts-dejavu-core \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY python /app/python
COPY gen /app/gen
RUN pip install --no-cache-dir -e /app/python/packages/officeconvert -e /app/python/packages/server
EXPOSE 8080
CMD ["uvicorn", "officeconvert_server.app:app", "--host", "0.0.0.0", "--port", "8080"]
+23
View File
@@ -0,0 +1,23 @@
SHELL := /bin/sh
BUF ?= /Users/end/go/bin/buf
.PHONY: buf-lint buf-generate py-sync go-test compose-up compose-up-dev
buf-lint:
"$(BUF)" lint
buf-generate:
"$(BUF)" generate
py-sync:
uv sync --project python
go-test:
cd clients/go && go test ./...
compose-up:
docker compose --env-file .env.example -f deploy/docker-compose.yml up --build
compose-up-dev:
docker compose --env-file .env.example -f deploy/docker-compose.dev.yml up
+26 -6
View File
@@ -1,13 +1,33 @@
# officeconvert
officeconvert is a toolset to convert common office document types images using LibreOffice (`soffice`). This repository provides three classes of projects:
officeconvert is a multimodule conversion toolkit for turning presentation files into
typed `SlideDeck` artifacts with rendered slide images and notes. The repository is
organized around Protocol Buffer schemas with ConnectRPC code generation for both server
and client compatibility.
- Python library responsible for actual conversion work
- Connect gRPC Python server for easy microservice deployment
- Client libraries for assorted languages
## Modules
APIs are built on typed Protocol Buffer schemas with codegen for the server and clients based on the primary schema.
- `proto/` contains protobuf schemas and RPC definitions.
- `gen/python` and `gen/go` contain generated protocol and Connect code.
- `python/packages/officeconvert` is the core conversion library (PPTX -> PDF -> images + notes).
- `python/packages/server` is the ConnectRPC Python server with MinIO orchestration.
- `clients/go` is the first client library with layered orchestration helpers.
- `deploy/` contains production-ish and dev Docker Compose files.
## Supported Document Types
Currently, only PPTX and ODP documents are supported. At the time of writing, PDF and image conversion is planned, however, presentation documents can currently only be converted to a custom `SlideDeck` type that encodes slides as images and includes richly-formatted slide notes.
MVP currently supports **PPTX only** and produces a `SlideDeck` result containing:
- ordered slide image URLs
- plain-text notes per slide
## Quick Commands
Use the root `Makefile`:
- `make buf-lint` to lint protobufs
- `make buf-generate` to regenerate Go and Python types
- `make py-sync` to sync Python workspace dependencies with uv
- `make go-test` to run Go client tests
- `make compose-up` to run server + MinIO
- `make compose-up-dev` to run MinIO only
+16
View File
@@ -0,0 +1,16 @@
version: v2
plugins:
- remote: buf.build/protocolbuffers/python
out: gen/python
- remote: buf.build/protocolbuffers/pyi
out: gen/python
- remote: buf.build/connectrpc/python
out: gen/python
- remote: buf.build/protocolbuffers/go
out: gen/go
opt:
- paths=source_relative
- remote: buf.build/connectrpc/go
out: gen/go
opt:
- paths=source_relative
+9
View File
@@ -0,0 +1,9 @@
version: v2
modules:
- path: proto
lint:
use:
- STANDARD
breaking:
use:
- FILE
+12
View File
@@ -0,0 +1,12 @@
module github.com/end/officeconvert/clients/go
go 1.25.0
require (
connectrpc.com/connect v1.19.1
github.com/end/officeconvert v0.0.0
)
require google.golang.org/protobuf v1.36.11 // indirect
replace github.com/end/officeconvert => ../..
+6
View File
@@ -0,0 +1,6 @@
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+147
View File
@@ -0,0 +1,147 @@
// Package officeconvertclient provides typed Connect RPC helpers for conversions.
package officeconvertclient
import (
"context"
"errors"
"net/http"
"time"
"connectrpc.com/connect"
officeconvertapiv1 "github.com/end/officeconvert/gen/go/officeconvertapi/v1"
"github.com/end/officeconvert/gen/go/officeconvertapi/v1/officeconvertapiv1connect"
)
const defaultPollInterval = 2 * time.Second
// Client wraps the generated Connect client with orchestration-focused helpers.
type Client struct {
rpc officeconvertapiv1connect.ConversionServiceClient
httpClient *http.Client
baseURL string
pollInterval time.Duration
}
// NewClient creates a new typed Officeconvert client.
func NewClient(baseURL string, httpClient *http.Client, options ...connect.ClientOption) *Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
return &Client{
rpc: officeconvertapiv1connect.NewConversionServiceClient(httpClient, baseURL, options...),
httpClient: httpClient,
baseURL: baseURL,
pollInterval: defaultPollInterval,
}
}
// SetPollInterval configures the polling cadence used by WaitForCompletion.
func (c *Client) SetPollInterval(interval time.Duration) {
if interval > 0 {
c.pollInterval = interval
}
}
// CreateConversion starts a conversion session and returns upload metadata.
func (c *Client) CreateConversion(
ctx context.Context,
sourceFilename string,
) (*officeconvertapiv1.CreateConversionResponse, error) {
req := connect.NewRequest(&officeconvertapiv1.CreateConversionRequest{
SourceFilename: sourceFilename,
})
res, err := c.rpc.CreateConversion(ctx, req)
if err != nil {
return nil, err
}
return res.Msg, nil
}
// StartConversion signals that upload is complete and conversion can begin.
func (c *Client) StartConversion(
ctx context.Context,
conversionID string,
) (*officeconvertapiv1.StartConversionResponse, error) {
req := connect.NewRequest(&officeconvertapiv1.StartConversionRequest{
ConversionId: conversionID,
})
res, err := c.rpc.StartConversion(ctx, req)
if err != nil {
return nil, err
}
return res.Msg, nil
}
// GetConversionStatus returns the latest status for a conversion session.
func (c *Client) GetConversionStatus(
ctx context.Context,
conversionID string,
) (*officeconvertapiv1.GetConversionStatusResponse, error) {
req := connect.NewRequest(&officeconvertapiv1.GetConversionStatusRequest{
ConversionId: conversionID,
})
res, err := c.rpc.GetConversionStatus(ctx, req)
if err != nil {
return nil, err
}
return res.Msg, nil
}
// GetSlideDeck retrieves the final converted deck response.
func (c *Client) GetSlideDeck(
ctx context.Context,
conversionID string,
) (*officeconvertapiv1.GetSlideDeckResponse, error) {
req := connect.NewRequest(&officeconvertapiv1.GetSlideDeckRequest{
ConversionId: conversionID,
})
res, err := c.rpc.GetSlideDeck(ctx, req)
if err != nil {
return nil, err
}
return res.Msg, nil
}
// DeleteConversion triggers immediate resource cleanup for a session.
func (c *Client) DeleteConversion(
ctx context.Context,
conversionID string,
) (*officeconvertapiv1.DeleteConversionResponse, error) {
req := connect.NewRequest(&officeconvertapiv1.DeleteConversionRequest{
ConversionId: conversionID,
})
res, err := c.rpc.DeleteConversion(ctx, req)
if err != nil {
return nil, err
}
return res.Msg, nil
}
// WaitForCompletion polls status until terminal completion or context cancellation.
func (c *Client) WaitForCompletion(
ctx context.Context,
conversionID string,
) (*officeconvertapiv1.GetConversionStatusResponse, error) {
ticker := time.NewTicker(c.pollInterval)
defer ticker.Stop()
for {
status, err := c.GetConversionStatus(ctx, conversionID)
if err != nil {
return nil, err
}
switch status.Status {
case officeconvertapiv1.ConversionStatus_CONVERSION_STATUS_SUCCEEDED:
return status, nil
case officeconvertapiv1.ConversionStatus_CONVERSION_STATUS_FAILED:
return nil, errors.New(status.ErrorMessage)
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
}
}
}
@@ -0,0 +1,84 @@
package officeconvertclient
import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
officeconvertapiv1 "github.com/end/officeconvert/gen/go/officeconvertapi/v1"
)
// DownloadedArtifact records a downloaded slide image destination path.
type DownloadedArtifact struct {
SlideIndex int32
LocalPath string
}
// DownloadArtifacts fetches all slide image URLs from a slide deck into outputDir.
func (c *Client) DownloadArtifacts(
ctx context.Context,
slideDeck *officeconvertapiv1.SlideDeck,
outputDir string,
) ([]DownloadedArtifact, error) {
if slideDeck == nil {
return nil, fmt.Errorf("slide deck is nil")
}
if err := os.MkdirAll(outputDir, 0o755); err != nil {
return nil, fmt.Errorf("create output directory: %w", err)
}
downloaded := make([]DownloadedArtifact, 0, len(slideDeck.Slides))
for _, slide := range slideDeck.Slides {
localPath := filepath.Join(outputDir, fmt.Sprintf("slide-%04d%s", slide.Index, inferImageExt(slide.ImageUrl)))
if err := c.downloadFile(ctx, slide.ImageUrl, localPath); err != nil {
return nil, err
}
downloaded = append(downloaded, DownloadedArtifact{
SlideIndex: slide.Index,
LocalPath: localPath,
})
}
return downloaded, nil
}
func (c *Client) downloadFile(ctx context.Context, sourceURL string, destinationPath string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
if err != nil {
return fmt.Errorf("build download request: %w", err)
}
res, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("download artifact: %w", err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf("download failed with status %d", res.StatusCode)
}
file, err := os.Create(destinationPath)
if err != nil {
return fmt.Errorf("create artifact file: %w", err)
}
defer file.Close()
if _, err := io.Copy(file, res.Body); err != nil {
return fmt.Errorf("write artifact file: %w", err)
}
return nil
}
func inferImageExt(imageURL string) string {
switch {
case strings.Contains(imageURL, ".jpeg") || strings.Contains(imageURL, ".jpg"):
return ".jpg"
case strings.Contains(imageURL, ".webp"):
return ".webp"
default:
return ".png"
}
}
@@ -0,0 +1,45 @@
package officeconvertclient
import (
"context"
"fmt"
"path/filepath"
officeconvertapiv1 "github.com/end/officeconvert/gen/go/officeconvertapi/v1"
)
// ConversionResult groups the terminal status and deck payload for one conversion.
type ConversionResult struct {
Status *officeconvertapiv1.GetConversionStatusResponse
Deck *officeconvertapiv1.SlideDeck
}
// ConvertPPTXFile runs the full create-upload-start-wait-fetch flow.
func (c *Client) ConvertPPTXFile(ctx context.Context, localPPTXPath string) (*ConversionResult, error) {
createRes, err := c.CreateConversion(ctx, filepath.Base(localPPTXPath))
if err != nil {
return nil, fmt.Errorf("create conversion: %w", err)
}
if err := c.UploadPPTX(ctx, createRes.UploadUrl, localPPTXPath); err != nil {
return nil, err
}
if _, err := c.StartConversion(ctx, createRes.ConversionId); err != nil {
return nil, fmt.Errorf("start conversion: %w", err)
}
statusRes, err := c.WaitForCompletion(ctx, createRes.ConversionId)
if err != nil {
return nil, fmt.Errorf("wait for completion: %w", err)
}
deckRes, err := c.GetSlideDeck(ctx, createRes.ConversionId)
if err != nil {
return nil, fmt.Errorf("get slide deck: %w", err)
}
return &ConversionResult{
Status: statusRes,
Deck: deckRes.SlideDeck,
}, nil
}
+43
View File
@@ -0,0 +1,43 @@
package officeconvertclient
import (
"context"
"fmt"
"io"
"net/http"
"os"
)
// UploadPPTX uploads a local PPTX using the presigned URL from CreateConversion.
func (c *Client) UploadPPTX(
ctx context.Context,
uploadURL string,
localPPTXPath string,
) error {
file, err := os.Open(localPPTXPath)
if err != nil {
return fmt.Errorf("open pptx: %w", err)
}
defer file.Close()
return c.uploadReader(ctx, uploadURL, file)
}
func (c *Client) uploadReader(ctx context.Context, uploadURL string, body io.Reader) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, body)
if err != nil {
return fmt.Errorf("build upload request: %w", err)
}
req.Header.Set("Content-Type", "application/vnd.openxmlformats-officedocument.presentationml.presentation")
res, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("upload pptx: %w", err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf("upload failed with status %d", res.StatusCode)
}
return nil
}
+15
View File
@@ -0,0 +1,15 @@
services:
minio:
image: minio/minio:RELEASE.2026-02-17T00-53-00Z
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
volumes:
minio_data:
+32
View File
@@ -0,0 +1,32 @@
services:
minio:
image: minio/minio:RELEASE.2026-02-17T00-53-00Z
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
server:
build:
context: ..
dockerfile: Dockerfile.server
depends_on:
- minio
environment:
MINIO_ENDPOINT: ${MINIO_ENDPOINT:-minio:9000}
MINIO_PUBLIC_ENDPOINT: ${MINIO_PUBLIC_ENDPOINT:-localhost:9000}
MINIO_USE_SSL: ${MINIO_USE_SSL:-false}
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}
MINIO_SESSION_TTL_SECONDS: ${MINIO_SESSION_TTL_SECONDS:-3600}
CONVERSION_CLEANUP_DELAY_SECONDS: ${CONVERSION_CLEANUP_DELAY_SECONDS:-3600}
ports:
- "8080:8080"
volumes:
minio_data:
+873
View File
@@ -0,0 +1,873 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc (unknown)
// source: officeconvertapi/v1/conversion.proto
package officeconvertapiv1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// ConversionStatus represents the lifecycle state of a conversion request.
type ConversionStatus int32
const (
ConversionStatus_CONVERSION_STATUS_UNSPECIFIED ConversionStatus = 0
ConversionStatus_CONVERSION_STATUS_PENDING ConversionStatus = 1
ConversionStatus_CONVERSION_STATUS_RUNNING ConversionStatus = 2
ConversionStatus_CONVERSION_STATUS_SUCCEEDED ConversionStatus = 3
ConversionStatus_CONVERSION_STATUS_FAILED ConversionStatus = 4
)
// Enum value maps for ConversionStatus.
var (
ConversionStatus_name = map[int32]string{
0: "CONVERSION_STATUS_UNSPECIFIED",
1: "CONVERSION_STATUS_PENDING",
2: "CONVERSION_STATUS_RUNNING",
3: "CONVERSION_STATUS_SUCCEEDED",
4: "CONVERSION_STATUS_FAILED",
}
ConversionStatus_value = map[string]int32{
"CONVERSION_STATUS_UNSPECIFIED": 0,
"CONVERSION_STATUS_PENDING": 1,
"CONVERSION_STATUS_RUNNING": 2,
"CONVERSION_STATUS_SUCCEEDED": 3,
"CONVERSION_STATUS_FAILED": 4,
}
)
func (x ConversionStatus) Enum() *ConversionStatus {
p := new(ConversionStatus)
*p = x
return p
}
func (x ConversionStatus) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ConversionStatus) Descriptor() protoreflect.EnumDescriptor {
return file_officeconvertapi_v1_conversion_proto_enumTypes[0].Descriptor()
}
func (ConversionStatus) Type() protoreflect.EnumType {
return &file_officeconvertapi_v1_conversion_proto_enumTypes[0]
}
func (x ConversionStatus) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ConversionStatus.Descriptor instead.
func (ConversionStatus) EnumDescriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{0}
}
// Slide contains extracted notes and the rendered image URL for one slide.
type Slide struct {
state protoimpl.MessageState `protogen:"open.v1"`
Index int32 `protobuf:"varint,1,opt,name=index,proto3" json:"index,omitempty"`
NotesPlain string `protobuf:"bytes,2,opt,name=notes_plain,json=notesPlain,proto3" json:"notes_plain,omitempty"`
ImageUrl string `protobuf:"bytes,3,opt,name=image_url,json=imageUrl,proto3" json:"image_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Slide) Reset() {
*x = Slide{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Slide) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Slide) ProtoMessage() {}
func (x *Slide) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Slide.ProtoReflect.Descriptor instead.
func (*Slide) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{0}
}
func (x *Slide) GetIndex() int32 {
if x != nil {
return x.Index
}
return 0
}
func (x *Slide) GetNotesPlain() string {
if x != nil {
return x.NotesPlain
}
return ""
}
func (x *Slide) GetImageUrl() string {
if x != nil {
return x.ImageUrl
}
return ""
}
// SlideDeck is the final structured conversion artifact.
type SlideDeck struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConversionId string `protobuf:"bytes,1,opt,name=conversion_id,json=conversionId,proto3" json:"conversion_id,omitempty"`
SourceFilename string `protobuf:"bytes,2,opt,name=source_filename,json=sourceFilename,proto3" json:"source_filename,omitempty"`
Slides []*Slide `protobuf:"bytes,3,rep,name=slides,proto3" json:"slides,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SlideDeck) Reset() {
*x = SlideDeck{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SlideDeck) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SlideDeck) ProtoMessage() {}
func (x *SlideDeck) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SlideDeck.ProtoReflect.Descriptor instead.
func (*SlideDeck) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{1}
}
func (x *SlideDeck) GetConversionId() string {
if x != nil {
return x.ConversionId
}
return ""
}
func (x *SlideDeck) GetSourceFilename() string {
if x != nil {
return x.SourceFilename
}
return ""
}
func (x *SlideDeck) GetSlides() []*Slide {
if x != nil {
return x.Slides
}
return nil
}
func (x *SlideDeck) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
// CreateConversionRequest starts a conversion session.
type CreateConversionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
SourceFilename string `protobuf:"bytes,1,opt,name=source_filename,json=sourceFilename,proto3" json:"source_filename,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateConversionRequest) Reset() {
*x = CreateConversionRequest{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateConversionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateConversionRequest) ProtoMessage() {}
func (x *CreateConversionRequest) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateConversionRequest.ProtoReflect.Descriptor instead.
func (*CreateConversionRequest) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{2}
}
func (x *CreateConversionRequest) GetSourceFilename() string {
if x != nil {
return x.SourceFilename
}
return ""
}
// CreateConversionResponse returns upload details for the session.
type CreateConversionResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConversionId string `protobuf:"bytes,1,opt,name=conversion_id,json=conversionId,proto3" json:"conversion_id,omitempty"`
UploadBucket string `protobuf:"bytes,2,opt,name=upload_bucket,json=uploadBucket,proto3" json:"upload_bucket,omitempty"`
UploadObjectKey string `protobuf:"bytes,3,opt,name=upload_object_key,json=uploadObjectKey,proto3" json:"upload_object_key,omitempty"`
UploadUrl string `protobuf:"bytes,4,opt,name=upload_url,json=uploadUrl,proto3" json:"upload_url,omitempty"`
ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateConversionResponse) Reset() {
*x = CreateConversionResponse{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateConversionResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateConversionResponse) ProtoMessage() {}
func (x *CreateConversionResponse) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateConversionResponse.ProtoReflect.Descriptor instead.
func (*CreateConversionResponse) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{3}
}
func (x *CreateConversionResponse) GetConversionId() string {
if x != nil {
return x.ConversionId
}
return ""
}
func (x *CreateConversionResponse) GetUploadBucket() string {
if x != nil {
return x.UploadBucket
}
return ""
}
func (x *CreateConversionResponse) GetUploadObjectKey() string {
if x != nil {
return x.UploadObjectKey
}
return ""
}
func (x *CreateConversionResponse) GetUploadUrl() string {
if x != nil {
return x.UploadUrl
}
return ""
}
func (x *CreateConversionResponse) GetExpiresAt() *timestamppb.Timestamp {
if x != nil {
return x.ExpiresAt
}
return nil
}
// StartConversionRequest requests conversion of an already uploaded PPTX.
type StartConversionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConversionId string `protobuf:"bytes,1,opt,name=conversion_id,json=conversionId,proto3" json:"conversion_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StartConversionRequest) Reset() {
*x = StartConversionRequest{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StartConversionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StartConversionRequest) ProtoMessage() {}
func (x *StartConversionRequest) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StartConversionRequest.ProtoReflect.Descriptor instead.
func (*StartConversionRequest) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{4}
}
func (x *StartConversionRequest) GetConversionId() string {
if x != nil {
return x.ConversionId
}
return ""
}
// StartConversionResponse returns the first known status after enqueue.
type StartConversionResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConversionId string `protobuf:"bytes,1,opt,name=conversion_id,json=conversionId,proto3" json:"conversion_id,omitempty"`
Status ConversionStatus `protobuf:"varint,2,opt,name=status,proto3,enum=officeconvertapi.v1.ConversionStatus" json:"status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StartConversionResponse) Reset() {
*x = StartConversionResponse{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StartConversionResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StartConversionResponse) ProtoMessage() {}
func (x *StartConversionResponse) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StartConversionResponse.ProtoReflect.Descriptor instead.
func (*StartConversionResponse) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{5}
}
func (x *StartConversionResponse) GetConversionId() string {
if x != nil {
return x.ConversionId
}
return ""
}
func (x *StartConversionResponse) GetStatus() ConversionStatus {
if x != nil {
return x.Status
}
return ConversionStatus_CONVERSION_STATUS_UNSPECIFIED
}
// GetConversionStatusRequest asks for a specific conversion status.
type GetConversionStatusRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConversionId string `protobuf:"bytes,1,opt,name=conversion_id,json=conversionId,proto3" json:"conversion_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetConversionStatusRequest) Reset() {
*x = GetConversionStatusRequest{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetConversionStatusRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetConversionStatusRequest) ProtoMessage() {}
func (x *GetConversionStatusRequest) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetConversionStatusRequest.ProtoReflect.Descriptor instead.
func (*GetConversionStatusRequest) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{6}
}
func (x *GetConversionStatusRequest) GetConversionId() string {
if x != nil {
return x.ConversionId
}
return ""
}
// GetConversionStatusResponse returns current status and optional error info.
type GetConversionStatusResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConversionId string `protobuf:"bytes,1,opt,name=conversion_id,json=conversionId,proto3" json:"conversion_id,omitempty"`
Status ConversionStatus `protobuf:"varint,2,opt,name=status,proto3,enum=officeconvertapi.v1.ConversionStatus" json:"status,omitempty"`
ErrorMessage string `protobuf:"bytes,3,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetConversionStatusResponse) Reset() {
*x = GetConversionStatusResponse{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetConversionStatusResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetConversionStatusResponse) ProtoMessage() {}
func (x *GetConversionStatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetConversionStatusResponse.ProtoReflect.Descriptor instead.
func (*GetConversionStatusResponse) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{7}
}
func (x *GetConversionStatusResponse) GetConversionId() string {
if x != nil {
return x.ConversionId
}
return ""
}
func (x *GetConversionStatusResponse) GetStatus() ConversionStatus {
if x != nil {
return x.Status
}
return ConversionStatus_CONVERSION_STATUS_UNSPECIFIED
}
func (x *GetConversionStatusResponse) GetErrorMessage() string {
if x != nil {
return x.ErrorMessage
}
return ""
}
func (x *GetConversionStatusResponse) GetUpdatedAt() *timestamppb.Timestamp {
if x != nil {
return x.UpdatedAt
}
return nil
}
// GetSlideDeckRequest fetches a completed deck.
type GetSlideDeckRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConversionId string `protobuf:"bytes,1,opt,name=conversion_id,json=conversionId,proto3" json:"conversion_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetSlideDeckRequest) Reset() {
*x = GetSlideDeckRequest{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetSlideDeckRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetSlideDeckRequest) ProtoMessage() {}
func (x *GetSlideDeckRequest) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetSlideDeckRequest.ProtoReflect.Descriptor instead.
func (*GetSlideDeckRequest) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{8}
}
func (x *GetSlideDeckRequest) GetConversionId() string {
if x != nil {
return x.ConversionId
}
return ""
}
// GetSlideDeckResponse contains the converted slide deck.
type GetSlideDeckResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
SlideDeck *SlideDeck `protobuf:"bytes,1,opt,name=slide_deck,json=slideDeck,proto3" json:"slide_deck,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetSlideDeckResponse) Reset() {
*x = GetSlideDeckResponse{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetSlideDeckResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetSlideDeckResponse) ProtoMessage() {}
func (x *GetSlideDeckResponse) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetSlideDeckResponse.ProtoReflect.Descriptor instead.
func (*GetSlideDeckResponse) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{9}
}
func (x *GetSlideDeckResponse) GetSlideDeck() *SlideDeck {
if x != nil {
return x.SlideDeck
}
return nil
}
// DeleteConversionRequest requests immediate cleanup.
type DeleteConversionRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConversionId string `protobuf:"bytes,1,opt,name=conversion_id,json=conversionId,proto3" json:"conversion_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteConversionRequest) Reset() {
*x = DeleteConversionRequest{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteConversionRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteConversionRequest) ProtoMessage() {}
func (x *DeleteConversionRequest) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteConversionRequest.ProtoReflect.Descriptor instead.
func (*DeleteConversionRequest) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{10}
}
func (x *DeleteConversionRequest) GetConversionId() string {
if x != nil {
return x.ConversionId
}
return ""
}
// DeleteConversionResponse confirms cleanup details.
type DeleteConversionResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ConversionId string `protobuf:"bytes,1,opt,name=conversion_id,json=conversionId,proto3" json:"conversion_id,omitempty"`
Deleted bool `protobuf:"varint,2,opt,name=deleted,proto3" json:"deleted,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteConversionResponse) Reset() {
*x = DeleteConversionResponse{}
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteConversionResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteConversionResponse) ProtoMessage() {}
func (x *DeleteConversionResponse) ProtoReflect() protoreflect.Message {
mi := &file_officeconvertapi_v1_conversion_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteConversionResponse.ProtoReflect.Descriptor instead.
func (*DeleteConversionResponse) Descriptor() ([]byte, []int) {
return file_officeconvertapi_v1_conversion_proto_rawDescGZIP(), []int{11}
}
func (x *DeleteConversionResponse) GetConversionId() string {
if x != nil {
return x.ConversionId
}
return ""
}
func (x *DeleteConversionResponse) GetDeleted() bool {
if x != nil {
return x.Deleted
}
return false
}
var File_officeconvertapi_v1_conversion_proto protoreflect.FileDescriptor
const file_officeconvertapi_v1_conversion_proto_rawDesc = "" +
"\n" +
"$officeconvertapi/v1/conversion.proto\x12\x13officeconvertapi.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"[\n" +
"\x05Slide\x12\x14\n" +
"\x05index\x18\x01 \x01(\x05R\x05index\x12\x1f\n" +
"\vnotes_plain\x18\x02 \x01(\tR\n" +
"notesPlain\x12\x1b\n" +
"\timage_url\x18\x03 \x01(\tR\bimageUrl\"\xc8\x01\n" +
"\tSlideDeck\x12#\n" +
"\rconversion_id\x18\x01 \x01(\tR\fconversionId\x12'\n" +
"\x0fsource_filename\x18\x02 \x01(\tR\x0esourceFilename\x122\n" +
"\x06slides\x18\x03 \x03(\v2\x1a.officeconvertapi.v1.SlideR\x06slides\x129\n" +
"\n" +
"created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\"B\n" +
"\x17CreateConversionRequest\x12'\n" +
"\x0fsource_filename\x18\x01 \x01(\tR\x0esourceFilename\"\xea\x01\n" +
"\x18CreateConversionResponse\x12#\n" +
"\rconversion_id\x18\x01 \x01(\tR\fconversionId\x12#\n" +
"\rupload_bucket\x18\x02 \x01(\tR\fuploadBucket\x12*\n" +
"\x11upload_object_key\x18\x03 \x01(\tR\x0fuploadObjectKey\x12\x1d\n" +
"\n" +
"upload_url\x18\x04 \x01(\tR\tuploadUrl\x129\n" +
"\n" +
"expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"=\n" +
"\x16StartConversionRequest\x12#\n" +
"\rconversion_id\x18\x01 \x01(\tR\fconversionId\"}\n" +
"\x17StartConversionResponse\x12#\n" +
"\rconversion_id\x18\x01 \x01(\tR\fconversionId\x12=\n" +
"\x06status\x18\x02 \x01(\x0e2%.officeconvertapi.v1.ConversionStatusR\x06status\"A\n" +
"\x1aGetConversionStatusRequest\x12#\n" +
"\rconversion_id\x18\x01 \x01(\tR\fconversionId\"\xe1\x01\n" +
"\x1bGetConversionStatusResponse\x12#\n" +
"\rconversion_id\x18\x01 \x01(\tR\fconversionId\x12=\n" +
"\x06status\x18\x02 \x01(\x0e2%.officeconvertapi.v1.ConversionStatusR\x06status\x12#\n" +
"\rerror_message\x18\x03 \x01(\tR\ferrorMessage\x129\n" +
"\n" +
"updated_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\":\n" +
"\x13GetSlideDeckRequest\x12#\n" +
"\rconversion_id\x18\x01 \x01(\tR\fconversionId\"U\n" +
"\x14GetSlideDeckResponse\x12=\n" +
"\n" +
"slide_deck\x18\x01 \x01(\v2\x1e.officeconvertapi.v1.SlideDeckR\tslideDeck\">\n" +
"\x17DeleteConversionRequest\x12#\n" +
"\rconversion_id\x18\x01 \x01(\tR\fconversionId\"Y\n" +
"\x18DeleteConversionResponse\x12#\n" +
"\rconversion_id\x18\x01 \x01(\tR\fconversionId\x12\x18\n" +
"\adeleted\x18\x02 \x01(\bR\adeleted*\xb2\x01\n" +
"\x10ConversionStatus\x12!\n" +
"\x1dCONVERSION_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n" +
"\x19CONVERSION_STATUS_PENDING\x10\x01\x12\x1d\n" +
"\x19CONVERSION_STATUS_RUNNING\x10\x02\x12\x1f\n" +
"\x1bCONVERSION_STATUS_SUCCEEDED\x10\x03\x12\x1c\n" +
"\x18CONVERSION_STATUS_FAILED\x10\x042\xcc\x04\n" +
"\x11ConversionService\x12q\n" +
"\x10CreateConversion\x12,.officeconvertapi.v1.CreateConversionRequest\x1a-.officeconvertapi.v1.CreateConversionResponse\"\x00\x12n\n" +
"\x0fStartConversion\x12+.officeconvertapi.v1.StartConversionRequest\x1a,.officeconvertapi.v1.StartConversionResponse\"\x00\x12z\n" +
"\x13GetConversionStatus\x12/.officeconvertapi.v1.GetConversionStatusRequest\x1a0.officeconvertapi.v1.GetConversionStatusResponse\"\x00\x12e\n" +
"\fGetSlideDeck\x12(.officeconvertapi.v1.GetSlideDeckRequest\x1a).officeconvertapi.v1.GetSlideDeckResponse\"\x00\x12q\n" +
"\x10DeleteConversion\x12,.officeconvertapi.v1.DeleteConversionRequest\x1a-.officeconvertapi.v1.DeleteConversionResponse\"\x00BLZJgithub.com/end/officeconvert/gen/go/officeconvertapi/v1;officeconvertapiv1b\x06proto3"
var (
file_officeconvertapi_v1_conversion_proto_rawDescOnce sync.Once
file_officeconvertapi_v1_conversion_proto_rawDescData []byte
)
func file_officeconvertapi_v1_conversion_proto_rawDescGZIP() []byte {
file_officeconvertapi_v1_conversion_proto_rawDescOnce.Do(func() {
file_officeconvertapi_v1_conversion_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_officeconvertapi_v1_conversion_proto_rawDesc), len(file_officeconvertapi_v1_conversion_proto_rawDesc)))
})
return file_officeconvertapi_v1_conversion_proto_rawDescData
}
var file_officeconvertapi_v1_conversion_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_officeconvertapi_v1_conversion_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_officeconvertapi_v1_conversion_proto_goTypes = []any{
(ConversionStatus)(0), // 0: officeconvertapi.v1.ConversionStatus
(*Slide)(nil), // 1: officeconvertapi.v1.Slide
(*SlideDeck)(nil), // 2: officeconvertapi.v1.SlideDeck
(*CreateConversionRequest)(nil), // 3: officeconvertapi.v1.CreateConversionRequest
(*CreateConversionResponse)(nil), // 4: officeconvertapi.v1.CreateConversionResponse
(*StartConversionRequest)(nil), // 5: officeconvertapi.v1.StartConversionRequest
(*StartConversionResponse)(nil), // 6: officeconvertapi.v1.StartConversionResponse
(*GetConversionStatusRequest)(nil), // 7: officeconvertapi.v1.GetConversionStatusRequest
(*GetConversionStatusResponse)(nil), // 8: officeconvertapi.v1.GetConversionStatusResponse
(*GetSlideDeckRequest)(nil), // 9: officeconvertapi.v1.GetSlideDeckRequest
(*GetSlideDeckResponse)(nil), // 10: officeconvertapi.v1.GetSlideDeckResponse
(*DeleteConversionRequest)(nil), // 11: officeconvertapi.v1.DeleteConversionRequest
(*DeleteConversionResponse)(nil), // 12: officeconvertapi.v1.DeleteConversionResponse
(*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp
}
var file_officeconvertapi_v1_conversion_proto_depIdxs = []int32{
1, // 0: officeconvertapi.v1.SlideDeck.slides:type_name -> officeconvertapi.v1.Slide
13, // 1: officeconvertapi.v1.SlideDeck.created_at:type_name -> google.protobuf.Timestamp
13, // 2: officeconvertapi.v1.CreateConversionResponse.expires_at:type_name -> google.protobuf.Timestamp
0, // 3: officeconvertapi.v1.StartConversionResponse.status:type_name -> officeconvertapi.v1.ConversionStatus
0, // 4: officeconvertapi.v1.GetConversionStatusResponse.status:type_name -> officeconvertapi.v1.ConversionStatus
13, // 5: officeconvertapi.v1.GetConversionStatusResponse.updated_at:type_name -> google.protobuf.Timestamp
2, // 6: officeconvertapi.v1.GetSlideDeckResponse.slide_deck:type_name -> officeconvertapi.v1.SlideDeck
3, // 7: officeconvertapi.v1.ConversionService.CreateConversion:input_type -> officeconvertapi.v1.CreateConversionRequest
5, // 8: officeconvertapi.v1.ConversionService.StartConversion:input_type -> officeconvertapi.v1.StartConversionRequest
7, // 9: officeconvertapi.v1.ConversionService.GetConversionStatus:input_type -> officeconvertapi.v1.GetConversionStatusRequest
9, // 10: officeconvertapi.v1.ConversionService.GetSlideDeck:input_type -> officeconvertapi.v1.GetSlideDeckRequest
11, // 11: officeconvertapi.v1.ConversionService.DeleteConversion:input_type -> officeconvertapi.v1.DeleteConversionRequest
4, // 12: officeconvertapi.v1.ConversionService.CreateConversion:output_type -> officeconvertapi.v1.CreateConversionResponse
6, // 13: officeconvertapi.v1.ConversionService.StartConversion:output_type -> officeconvertapi.v1.StartConversionResponse
8, // 14: officeconvertapi.v1.ConversionService.GetConversionStatus:output_type -> officeconvertapi.v1.GetConversionStatusResponse
10, // 15: officeconvertapi.v1.ConversionService.GetSlideDeck:output_type -> officeconvertapi.v1.GetSlideDeckResponse
12, // 16: officeconvertapi.v1.ConversionService.DeleteConversion:output_type -> officeconvertapi.v1.DeleteConversionResponse
12, // [12:17] is the sub-list for method output_type
7, // [7:12] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
}
func init() { file_officeconvertapi_v1_conversion_proto_init() }
func file_officeconvertapi_v1_conversion_proto_init() {
if File_officeconvertapi_v1_conversion_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_officeconvertapi_v1_conversion_proto_rawDesc), len(file_officeconvertapi_v1_conversion_proto_rawDesc)),
NumEnums: 1,
NumMessages: 12,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_officeconvertapi_v1_conversion_proto_goTypes,
DependencyIndexes: file_officeconvertapi_v1_conversion_proto_depIdxs,
EnumInfos: file_officeconvertapi_v1_conversion_proto_enumTypes,
MessageInfos: file_officeconvertapi_v1_conversion_proto_msgTypes,
}.Build()
File_officeconvertapi_v1_conversion_proto = out.File
file_officeconvertapi_v1_conversion_proto_goTypes = nil
file_officeconvertapi_v1_conversion_proto_depIdxs = nil
}
@@ -0,0 +1,236 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: officeconvertapi/v1/conversion.proto
package officeconvertapiv1connect
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
v1 "github.com/end/officeconvert/gen/go/officeconvertapi/v1"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// ConversionServiceName is the fully-qualified name of the ConversionService service.
ConversionServiceName = "officeconvertapi.v1.ConversionService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// ConversionServiceCreateConversionProcedure is the fully-qualified name of the ConversionService's
// CreateConversion RPC.
ConversionServiceCreateConversionProcedure = "/officeconvertapi.v1.ConversionService/CreateConversion"
// ConversionServiceStartConversionProcedure is the fully-qualified name of the ConversionService's
// StartConversion RPC.
ConversionServiceStartConversionProcedure = "/officeconvertapi.v1.ConversionService/StartConversion"
// ConversionServiceGetConversionStatusProcedure is the fully-qualified name of the
// ConversionService's GetConversionStatus RPC.
ConversionServiceGetConversionStatusProcedure = "/officeconvertapi.v1.ConversionService/GetConversionStatus"
// ConversionServiceGetSlideDeckProcedure is the fully-qualified name of the ConversionService's
// GetSlideDeck RPC.
ConversionServiceGetSlideDeckProcedure = "/officeconvertapi.v1.ConversionService/GetSlideDeck"
// ConversionServiceDeleteConversionProcedure is the fully-qualified name of the ConversionService's
// DeleteConversion RPC.
ConversionServiceDeleteConversionProcedure = "/officeconvertapi.v1.ConversionService/DeleteConversion"
)
// ConversionServiceClient is a client for the officeconvertapi.v1.ConversionService service.
type ConversionServiceClient interface {
// CreateConversion allocates a short-lived session and upload URL for a PPTX.
CreateConversion(context.Context, *connect.Request[v1.CreateConversionRequest]) (*connect.Response[v1.CreateConversionResponse], error)
// StartConversion marks upload completion and starts server-side conversion.
StartConversion(context.Context, *connect.Request[v1.StartConversionRequest]) (*connect.Response[v1.StartConversionResponse], error)
// GetConversionStatus returns state transitions for a conversion session.
GetConversionStatus(context.Context, *connect.Request[v1.GetConversionStatusRequest]) (*connect.Response[v1.GetConversionStatusResponse], error)
// GetSlideDeck fetches the final slide deck data after successful conversion.
GetSlideDeck(context.Context, *connect.Request[v1.GetSlideDeckRequest]) (*connect.Response[v1.GetSlideDeckResponse], error)
// DeleteConversion deletes session resources before automatic expiration.
DeleteConversion(context.Context, *connect.Request[v1.DeleteConversionRequest]) (*connect.Response[v1.DeleteConversionResponse], error)
}
// NewConversionServiceClient constructs a client for the officeconvertapi.v1.ConversionService
// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
// the connect.WithGRPC() or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewConversionServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ConversionServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
conversionServiceMethods := v1.File_officeconvertapi_v1_conversion_proto.Services().ByName("ConversionService").Methods()
return &conversionServiceClient{
createConversion: connect.NewClient[v1.CreateConversionRequest, v1.CreateConversionResponse](
httpClient,
baseURL+ConversionServiceCreateConversionProcedure,
connect.WithSchema(conversionServiceMethods.ByName("CreateConversion")),
connect.WithClientOptions(opts...),
),
startConversion: connect.NewClient[v1.StartConversionRequest, v1.StartConversionResponse](
httpClient,
baseURL+ConversionServiceStartConversionProcedure,
connect.WithSchema(conversionServiceMethods.ByName("StartConversion")),
connect.WithClientOptions(opts...),
),
getConversionStatus: connect.NewClient[v1.GetConversionStatusRequest, v1.GetConversionStatusResponse](
httpClient,
baseURL+ConversionServiceGetConversionStatusProcedure,
connect.WithSchema(conversionServiceMethods.ByName("GetConversionStatus")),
connect.WithClientOptions(opts...),
),
getSlideDeck: connect.NewClient[v1.GetSlideDeckRequest, v1.GetSlideDeckResponse](
httpClient,
baseURL+ConversionServiceGetSlideDeckProcedure,
connect.WithSchema(conversionServiceMethods.ByName("GetSlideDeck")),
connect.WithClientOptions(opts...),
),
deleteConversion: connect.NewClient[v1.DeleteConversionRequest, v1.DeleteConversionResponse](
httpClient,
baseURL+ConversionServiceDeleteConversionProcedure,
connect.WithSchema(conversionServiceMethods.ByName("DeleteConversion")),
connect.WithClientOptions(opts...),
),
}
}
// conversionServiceClient implements ConversionServiceClient.
type conversionServiceClient struct {
createConversion *connect.Client[v1.CreateConversionRequest, v1.CreateConversionResponse]
startConversion *connect.Client[v1.StartConversionRequest, v1.StartConversionResponse]
getConversionStatus *connect.Client[v1.GetConversionStatusRequest, v1.GetConversionStatusResponse]
getSlideDeck *connect.Client[v1.GetSlideDeckRequest, v1.GetSlideDeckResponse]
deleteConversion *connect.Client[v1.DeleteConversionRequest, v1.DeleteConversionResponse]
}
// CreateConversion calls officeconvertapi.v1.ConversionService.CreateConversion.
func (c *conversionServiceClient) CreateConversion(ctx context.Context, req *connect.Request[v1.CreateConversionRequest]) (*connect.Response[v1.CreateConversionResponse], error) {
return c.createConversion.CallUnary(ctx, req)
}
// StartConversion calls officeconvertapi.v1.ConversionService.StartConversion.
func (c *conversionServiceClient) StartConversion(ctx context.Context, req *connect.Request[v1.StartConversionRequest]) (*connect.Response[v1.StartConversionResponse], error) {
return c.startConversion.CallUnary(ctx, req)
}
// GetConversionStatus calls officeconvertapi.v1.ConversionService.GetConversionStatus.
func (c *conversionServiceClient) GetConversionStatus(ctx context.Context, req *connect.Request[v1.GetConversionStatusRequest]) (*connect.Response[v1.GetConversionStatusResponse], error) {
return c.getConversionStatus.CallUnary(ctx, req)
}
// GetSlideDeck calls officeconvertapi.v1.ConversionService.GetSlideDeck.
func (c *conversionServiceClient) GetSlideDeck(ctx context.Context, req *connect.Request[v1.GetSlideDeckRequest]) (*connect.Response[v1.GetSlideDeckResponse], error) {
return c.getSlideDeck.CallUnary(ctx, req)
}
// DeleteConversion calls officeconvertapi.v1.ConversionService.DeleteConversion.
func (c *conversionServiceClient) DeleteConversion(ctx context.Context, req *connect.Request[v1.DeleteConversionRequest]) (*connect.Response[v1.DeleteConversionResponse], error) {
return c.deleteConversion.CallUnary(ctx, req)
}
// ConversionServiceHandler is an implementation of the officeconvertapi.v1.ConversionService
// service.
type ConversionServiceHandler interface {
// CreateConversion allocates a short-lived session and upload URL for a PPTX.
CreateConversion(context.Context, *connect.Request[v1.CreateConversionRequest]) (*connect.Response[v1.CreateConversionResponse], error)
// StartConversion marks upload completion and starts server-side conversion.
StartConversion(context.Context, *connect.Request[v1.StartConversionRequest]) (*connect.Response[v1.StartConversionResponse], error)
// GetConversionStatus returns state transitions for a conversion session.
GetConversionStatus(context.Context, *connect.Request[v1.GetConversionStatusRequest]) (*connect.Response[v1.GetConversionStatusResponse], error)
// GetSlideDeck fetches the final slide deck data after successful conversion.
GetSlideDeck(context.Context, *connect.Request[v1.GetSlideDeckRequest]) (*connect.Response[v1.GetSlideDeckResponse], error)
// DeleteConversion deletes session resources before automatic expiration.
DeleteConversion(context.Context, *connect.Request[v1.DeleteConversionRequest]) (*connect.Response[v1.DeleteConversionResponse], error)
}
// NewConversionServiceHandler builds an HTTP handler from the service implementation. It returns
// the path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewConversionServiceHandler(svc ConversionServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
conversionServiceMethods := v1.File_officeconvertapi_v1_conversion_proto.Services().ByName("ConversionService").Methods()
conversionServiceCreateConversionHandler := connect.NewUnaryHandler(
ConversionServiceCreateConversionProcedure,
svc.CreateConversion,
connect.WithSchema(conversionServiceMethods.ByName("CreateConversion")),
connect.WithHandlerOptions(opts...),
)
conversionServiceStartConversionHandler := connect.NewUnaryHandler(
ConversionServiceStartConversionProcedure,
svc.StartConversion,
connect.WithSchema(conversionServiceMethods.ByName("StartConversion")),
connect.WithHandlerOptions(opts...),
)
conversionServiceGetConversionStatusHandler := connect.NewUnaryHandler(
ConversionServiceGetConversionStatusProcedure,
svc.GetConversionStatus,
connect.WithSchema(conversionServiceMethods.ByName("GetConversionStatus")),
connect.WithHandlerOptions(opts...),
)
conversionServiceGetSlideDeckHandler := connect.NewUnaryHandler(
ConversionServiceGetSlideDeckProcedure,
svc.GetSlideDeck,
connect.WithSchema(conversionServiceMethods.ByName("GetSlideDeck")),
connect.WithHandlerOptions(opts...),
)
conversionServiceDeleteConversionHandler := connect.NewUnaryHandler(
ConversionServiceDeleteConversionProcedure,
svc.DeleteConversion,
connect.WithSchema(conversionServiceMethods.ByName("DeleteConversion")),
connect.WithHandlerOptions(opts...),
)
return "/officeconvertapi.v1.ConversionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case ConversionServiceCreateConversionProcedure:
conversionServiceCreateConversionHandler.ServeHTTP(w, r)
case ConversionServiceStartConversionProcedure:
conversionServiceStartConversionHandler.ServeHTTP(w, r)
case ConversionServiceGetConversionStatusProcedure:
conversionServiceGetConversionStatusHandler.ServeHTTP(w, r)
case ConversionServiceGetSlideDeckProcedure:
conversionServiceGetSlideDeckHandler.ServeHTTP(w, r)
case ConversionServiceDeleteConversionProcedure:
conversionServiceDeleteConversionHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedConversionServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedConversionServiceHandler struct{}
func (UnimplementedConversionServiceHandler) CreateConversion(context.Context, *connect.Request[v1.CreateConversionRequest]) (*connect.Response[v1.CreateConversionResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("officeconvertapi.v1.ConversionService.CreateConversion is not implemented"))
}
func (UnimplementedConversionServiceHandler) StartConversion(context.Context, *connect.Request[v1.StartConversionRequest]) (*connect.Response[v1.StartConversionResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("officeconvertapi.v1.ConversionService.StartConversion is not implemented"))
}
func (UnimplementedConversionServiceHandler) GetConversionStatus(context.Context, *connect.Request[v1.GetConversionStatusRequest]) (*connect.Response[v1.GetConversionStatusResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("officeconvertapi.v1.ConversionService.GetConversionStatus is not implemented"))
}
func (UnimplementedConversionServiceHandler) GetSlideDeck(context.Context, *connect.Request[v1.GetSlideDeckRequest]) (*connect.Response[v1.GetSlideDeckResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("officeconvertapi.v1.ConversionService.GetSlideDeck is not implemented"))
}
func (UnimplementedConversionServiceHandler) DeleteConversion(context.Context, *connect.Request[v1.DeleteConversionRequest]) (*connect.Response[v1.DeleteConversionResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("officeconvertapi.v1.ConversionService.DeleteConversion is not implemented"))
}
@@ -0,0 +1,383 @@
# -*- coding: utf-8 -*-
# Generated by https://github.com/connectrpc/connect-python. DO NOT EDIT!
# source: officeconvertapi/v1/conversion.proto
from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Iterator, Mapping
from typing import Protocol
from connectrpc.client import ConnectClient, ConnectClientSync
from connectrpc.code import Code
from connectrpc.compression import Compression
from connectrpc.errors import ConnectError
from connectrpc.interceptor import Interceptor, InterceptorSync
from connectrpc.method import IdempotencyLevel, MethodInfo
from connectrpc.request import Headers, RequestContext
from connectrpc.server import ConnectASGIApplication, ConnectWSGIApplication, Endpoint, EndpointSync
import officeconvertapi.v1.conversion_pb2 as officeconvertapi_dot_v1_dot_conversion__pb2
class ConversionService(Protocol):
async def create_conversion(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
async def start_conversion(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
async def get_conversion_status(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
async def get_slide_deck(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
async def delete_conversion(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
class ConversionServiceASGIApplication(ConnectASGIApplication[ConversionService]):
def __init__(self, service: ConversionService | AsyncGenerator[ConversionService], *, interceptors: Iterable[Interceptor]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None) -> None:
super().__init__(
service=service,
endpoints=lambda svc: {
"/officeconvertapi.v1.ConversionService/CreateConversion": Endpoint.unary(
method=MethodInfo(
name="CreateConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=svc.create_conversion,
),
"/officeconvertapi.v1.ConversionService/StartConversion": Endpoint.unary(
method=MethodInfo(
name="StartConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=svc.start_conversion,
),
"/officeconvertapi.v1.ConversionService/GetConversionStatus": Endpoint.unary(
method=MethodInfo(
name="GetConversionStatus",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=svc.get_conversion_status,
),
"/officeconvertapi.v1.ConversionService/GetSlideDeck": Endpoint.unary(
method=MethodInfo(
name="GetSlideDeck",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=svc.get_slide_deck,
),
"/officeconvertapi.v1.ConversionService/DeleteConversion": Endpoint.unary(
method=MethodInfo(
name="DeleteConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=svc.delete_conversion,
),
},
interceptors=interceptors,
read_max_bytes=read_max_bytes,
compressions=compressions,
)
@property
def path(self) -> str:
"""Returns the URL path to mount the application to when serving multiple applications."""
return "/officeconvertapi.v1.ConversionService"
class ConversionServiceClient(ConnectClient):
async def create_conversion(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionResponse:
return await self.execute_unary(
request=request,
method=MethodInfo(
name="CreateConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
async def start_conversion(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionResponse:
return await self.execute_unary(
request=request,
method=MethodInfo(
name="StartConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
async def get_conversion_status(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusResponse:
return await self.execute_unary(
request=request,
method=MethodInfo(
name="GetConversionStatus",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
async def get_slide_deck(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckResponse:
return await self.execute_unary(
request=request,
method=MethodInfo(
name="GetSlideDeck",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
async def delete_conversion(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionResponse:
return await self.execute_unary(
request=request,
method=MethodInfo(
name="DeleteConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
class ConversionServiceSync(Protocol):
def create_conversion(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
def start_conversion(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
def get_conversion_status(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
def get_slide_deck(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
def delete_conversion(self, request: officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionRequest, ctx: RequestContext) -> officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionResponse:
raise ConnectError(Code.UNIMPLEMENTED, "Not implemented")
class ConversionServiceWSGIApplication(ConnectWSGIApplication):
def __init__(self, service: ConversionServiceSync, interceptors: Iterable[InterceptorSync]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None) -> None:
super().__init__(
endpoints={
"/officeconvertapi.v1.ConversionService/CreateConversion": EndpointSync.unary(
method=MethodInfo(
name="CreateConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=service.create_conversion,
),
"/officeconvertapi.v1.ConversionService/StartConversion": EndpointSync.unary(
method=MethodInfo(
name="StartConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=service.start_conversion,
),
"/officeconvertapi.v1.ConversionService/GetConversionStatus": EndpointSync.unary(
method=MethodInfo(
name="GetConversionStatus",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=service.get_conversion_status,
),
"/officeconvertapi.v1.ConversionService/GetSlideDeck": EndpointSync.unary(
method=MethodInfo(
name="GetSlideDeck",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=service.get_slide_deck,
),
"/officeconvertapi.v1.ConversionService/DeleteConversion": EndpointSync.unary(
method=MethodInfo(
name="DeleteConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
function=service.delete_conversion,
),
},
interceptors=interceptors,
read_max_bytes=read_max_bytes,
compressions=compressions,
)
@property
def path(self) -> str:
"""Returns the URL path to mount the application to when serving multiple applications."""
return "/officeconvertapi.v1.ConversionService"
class ConversionServiceClientSync(ConnectClientSync):
def create_conversion(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionResponse:
return self.execute_unary(
request=request,
method=MethodInfo(
name="CreateConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.CreateConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
def start_conversion(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionResponse:
return self.execute_unary(
request=request,
method=MethodInfo(
name="StartConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.StartConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
def get_conversion_status(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusResponse:
return self.execute_unary(
request=request,
method=MethodInfo(
name="GetConversionStatus",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.GetConversionStatusResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
def get_slide_deck(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckResponse:
return self.execute_unary(
request=request,
method=MethodInfo(
name="GetSlideDeck",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.GetSlideDeckResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
def delete_conversion(
self,
request: officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionRequest,
*,
headers: Headers | Mapping[str, str] | None = None,
timeout_ms: int | None = None,
) -> officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionResponse:
return self.execute_unary(
request=request,
method=MethodInfo(
name="DeleteConversion",
service_name="officeconvertapi.v1.ConversionService",
input=officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionRequest,
output=officeconvertapi_dot_v1_dot_conversion__pb2.DeleteConversionResponse,
idempotency_level=IdempotencyLevel.UNKNOWN,
),
headers=headers,
timeout_ms=timeout_ms,
)
@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: officeconvertapi/v1/conversion.proto
# Protobuf Python Version: 7.34.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
7,
34,
1,
'',
'officeconvertapi/v1/conversion.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$officeconvertapi/v1/conversion.proto\x12\x13officeconvertapi.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"[\n\x05Slide\x12\x14\n\x05index\x18\x01 \x01(\x05R\x05index\x12\x1f\n\x0bnotes_plain\x18\x02 \x01(\tR\nnotesPlain\x12\x1b\n\timage_url\x18\x03 \x01(\tR\x08imageUrl\"\xc8\x01\n\tSlideDeck\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12\'\n\x0fsource_filename\x18\x02 \x01(\tR\x0esourceFilename\x12\x32\n\x06slides\x18\x03 \x03(\x0b\x32\x1a.officeconvertapi.v1.SlideR\x06slides\x12\x39\n\ncreated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\"B\n\x17\x43reateConversionRequest\x12\'\n\x0fsource_filename\x18\x01 \x01(\tR\x0esourceFilename\"\xea\x01\n\x18\x43reateConversionResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12#\n\rupload_bucket\x18\x02 \x01(\tR\x0cuploadBucket\x12*\n\x11upload_object_key\x18\x03 \x01(\tR\x0fuploadObjectKey\x12\x1d\n\nupload_url\x18\x04 \x01(\tR\tuploadUrl\x12\x39\n\nexpires_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\texpiresAt\"=\n\x16StartConversionRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"}\n\x17StartConversionResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12=\n\x06status\x18\x02 \x01(\x0e\x32%.officeconvertapi.v1.ConversionStatusR\x06status\"A\n\x1aGetConversionStatusRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"\xe1\x01\n\x1bGetConversionStatusResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12=\n\x06status\x18\x02 \x01(\x0e\x32%.officeconvertapi.v1.ConversionStatusR\x06status\x12#\n\rerror_message\x18\x03 \x01(\tR\x0c\x65rrorMessage\x12\x39\n\nupdated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tupdatedAt\":\n\x13GetSlideDeckRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"U\n\x14GetSlideDeckResponse\x12=\n\nslide_deck\x18\x01 \x01(\x0b\x32\x1e.officeconvertapi.v1.SlideDeckR\tslideDeck\">\n\x17\x44\x65leteConversionRequest\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\"Y\n\x18\x44\x65leteConversionResponse\x12#\n\rconversion_id\x18\x01 \x01(\tR\x0c\x63onversionId\x12\x18\n\x07\x64\x65leted\x18\x02 \x01(\x08R\x07\x64\x65leted*\xb2\x01\n\x10\x43onversionStatus\x12!\n\x1d\x43ONVERSION_STATUS_UNSPECIFIED\x10\x00\x12\x1d\n\x19\x43ONVERSION_STATUS_PENDING\x10\x01\x12\x1d\n\x19\x43ONVERSION_STATUS_RUNNING\x10\x02\x12\x1f\n\x1b\x43ONVERSION_STATUS_SUCCEEDED\x10\x03\x12\x1c\n\x18\x43ONVERSION_STATUS_FAILED\x10\x04\x32\xcc\x04\n\x11\x43onversionService\x12q\n\x10\x43reateConversion\x12,.officeconvertapi.v1.CreateConversionRequest\x1a-.officeconvertapi.v1.CreateConversionResponse\"\x00\x12n\n\x0fStartConversion\x12+.officeconvertapi.v1.StartConversionRequest\x1a,.officeconvertapi.v1.StartConversionResponse\"\x00\x12z\n\x13GetConversionStatus\x12/.officeconvertapi.v1.GetConversionStatusRequest\x1a\x30.officeconvertapi.v1.GetConversionStatusResponse\"\x00\x12\x65\n\x0cGetSlideDeck\x12(.officeconvertapi.v1.GetSlideDeckRequest\x1a).officeconvertapi.v1.GetSlideDeckResponse\"\x00\x12q\n\x10\x44\x65leteConversion\x12,.officeconvertapi.v1.DeleteConversionRequest\x1a-.officeconvertapi.v1.DeleteConversionResponse\"\x00\x42LZJgithub.com/end/officeconvert/gen/go/officeconvertapi/v1;officeconvertapiv1b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'officeconvertapi.v1.conversion_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
_globals['DESCRIPTOR']._loaded_options = None
_globals['DESCRIPTOR']._serialized_options = b'ZJgithub.com/end/officeconvert/gen/go/officeconvertapi/v1;officeconvertapiv1'
_globals['_CONVERSIONSTATUS']._serialized_start=1483
_globals['_CONVERSIONSTATUS']._serialized_end=1661
_globals['_SLIDE']._serialized_start=94
_globals['_SLIDE']._serialized_end=185
_globals['_SLIDEDECK']._serialized_start=188
_globals['_SLIDEDECK']._serialized_end=388
_globals['_CREATECONVERSIONREQUEST']._serialized_start=390
_globals['_CREATECONVERSIONREQUEST']._serialized_end=456
_globals['_CREATECONVERSIONRESPONSE']._serialized_start=459
_globals['_CREATECONVERSIONRESPONSE']._serialized_end=693
_globals['_STARTCONVERSIONREQUEST']._serialized_start=695
_globals['_STARTCONVERSIONREQUEST']._serialized_end=756
_globals['_STARTCONVERSIONRESPONSE']._serialized_start=758
_globals['_STARTCONVERSIONRESPONSE']._serialized_end=883
_globals['_GETCONVERSIONSTATUSREQUEST']._serialized_start=885
_globals['_GETCONVERSIONSTATUSREQUEST']._serialized_end=950
_globals['_GETCONVERSIONSTATUSRESPONSE']._serialized_start=953
_globals['_GETCONVERSIONSTATUSRESPONSE']._serialized_end=1178
_globals['_GETSLIDEDECKREQUEST']._serialized_start=1180
_globals['_GETSLIDEDECKREQUEST']._serialized_end=1238
_globals['_GETSLIDEDECKRESPONSE']._serialized_start=1240
_globals['_GETSLIDEDECKRESPONSE']._serialized_end=1325
_globals['_DELETECONVERSIONREQUEST']._serialized_start=1327
_globals['_DELETECONVERSIONREQUEST']._serialized_end=1389
_globals['_DELETECONVERSIONRESPONSE']._serialized_start=1391
_globals['_DELETECONVERSIONRESPONSE']._serialized_end=1480
_globals['_CONVERSIONSERVICE']._serialized_start=1664
_globals['_CONVERSIONSERVICE']._serialized_end=2252
# @@protoc_insertion_point(module_scope)
@@ -0,0 +1,124 @@
import datetime
from google.protobuf import timestamp_pb2 as _timestamp_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Iterable as _Iterable, Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class ConversionStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
CONVERSION_STATUS_UNSPECIFIED: _ClassVar[ConversionStatus]
CONVERSION_STATUS_PENDING: _ClassVar[ConversionStatus]
CONVERSION_STATUS_RUNNING: _ClassVar[ConversionStatus]
CONVERSION_STATUS_SUCCEEDED: _ClassVar[ConversionStatus]
CONVERSION_STATUS_FAILED: _ClassVar[ConversionStatus]
CONVERSION_STATUS_UNSPECIFIED: ConversionStatus
CONVERSION_STATUS_PENDING: ConversionStatus
CONVERSION_STATUS_RUNNING: ConversionStatus
CONVERSION_STATUS_SUCCEEDED: ConversionStatus
CONVERSION_STATUS_FAILED: ConversionStatus
class Slide(_message.Message):
__slots__ = ("index", "notes_plain", "image_url")
INDEX_FIELD_NUMBER: _ClassVar[int]
NOTES_PLAIN_FIELD_NUMBER: _ClassVar[int]
IMAGE_URL_FIELD_NUMBER: _ClassVar[int]
index: int
notes_plain: str
image_url: str
def __init__(self, index: _Optional[int] = ..., notes_plain: _Optional[str] = ..., image_url: _Optional[str] = ...) -> None: ...
class SlideDeck(_message.Message):
__slots__ = ("conversion_id", "source_filename", "slides", "created_at")
CONVERSION_ID_FIELD_NUMBER: _ClassVar[int]
SOURCE_FILENAME_FIELD_NUMBER: _ClassVar[int]
SLIDES_FIELD_NUMBER: _ClassVar[int]
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
conversion_id: str
source_filename: str
slides: _containers.RepeatedCompositeFieldContainer[Slide]
created_at: _timestamp_pb2.Timestamp
def __init__(self, conversion_id: _Optional[str] = ..., source_filename: _Optional[str] = ..., slides: _Optional[_Iterable[_Union[Slide, _Mapping]]] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class CreateConversionRequest(_message.Message):
__slots__ = ("source_filename",)
SOURCE_FILENAME_FIELD_NUMBER: _ClassVar[int]
source_filename: str
def __init__(self, source_filename: _Optional[str] = ...) -> None: ...
class CreateConversionResponse(_message.Message):
__slots__ = ("conversion_id", "upload_bucket", "upload_object_key", "upload_url", "expires_at")
CONVERSION_ID_FIELD_NUMBER: _ClassVar[int]
UPLOAD_BUCKET_FIELD_NUMBER: _ClassVar[int]
UPLOAD_OBJECT_KEY_FIELD_NUMBER: _ClassVar[int]
UPLOAD_URL_FIELD_NUMBER: _ClassVar[int]
EXPIRES_AT_FIELD_NUMBER: _ClassVar[int]
conversion_id: str
upload_bucket: str
upload_object_key: str
upload_url: str
expires_at: _timestamp_pb2.Timestamp
def __init__(self, conversion_id: _Optional[str] = ..., upload_bucket: _Optional[str] = ..., upload_object_key: _Optional[str] = ..., upload_url: _Optional[str] = ..., expires_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class StartConversionRequest(_message.Message):
__slots__ = ("conversion_id",)
CONVERSION_ID_FIELD_NUMBER: _ClassVar[int]
conversion_id: str
def __init__(self, conversion_id: _Optional[str] = ...) -> None: ...
class StartConversionResponse(_message.Message):
__slots__ = ("conversion_id", "status")
CONVERSION_ID_FIELD_NUMBER: _ClassVar[int]
STATUS_FIELD_NUMBER: _ClassVar[int]
conversion_id: str
status: ConversionStatus
def __init__(self, conversion_id: _Optional[str] = ..., status: _Optional[_Union[ConversionStatus, str]] = ...) -> None: ...
class GetConversionStatusRequest(_message.Message):
__slots__ = ("conversion_id",)
CONVERSION_ID_FIELD_NUMBER: _ClassVar[int]
conversion_id: str
def __init__(self, conversion_id: _Optional[str] = ...) -> None: ...
class GetConversionStatusResponse(_message.Message):
__slots__ = ("conversion_id", "status", "error_message", "updated_at")
CONVERSION_ID_FIELD_NUMBER: _ClassVar[int]
STATUS_FIELD_NUMBER: _ClassVar[int]
ERROR_MESSAGE_FIELD_NUMBER: _ClassVar[int]
UPDATED_AT_FIELD_NUMBER: _ClassVar[int]
conversion_id: str
status: ConversionStatus
error_message: str
updated_at: _timestamp_pb2.Timestamp
def __init__(self, conversion_id: _Optional[str] = ..., status: _Optional[_Union[ConversionStatus, str]] = ..., error_message: _Optional[str] = ..., updated_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ...
class GetSlideDeckRequest(_message.Message):
__slots__ = ("conversion_id",)
CONVERSION_ID_FIELD_NUMBER: _ClassVar[int]
conversion_id: str
def __init__(self, conversion_id: _Optional[str] = ...) -> None: ...
class GetSlideDeckResponse(_message.Message):
__slots__ = ("slide_deck",)
SLIDE_DECK_FIELD_NUMBER: _ClassVar[int]
slide_deck: SlideDeck
def __init__(self, slide_deck: _Optional[_Union[SlideDeck, _Mapping]] = ...) -> None: ...
class DeleteConversionRequest(_message.Message):
__slots__ = ("conversion_id",)
CONVERSION_ID_FIELD_NUMBER: _ClassVar[int]
conversion_id: str
def __init__(self, conversion_id: _Optional[str] = ...) -> None: ...
class DeleteConversionResponse(_message.Message):
__slots__ = ("conversion_id", "deleted")
CONVERSION_ID_FIELD_NUMBER: _ClassVar[int]
DELETED_FIELD_NUMBER: _ClassVar[int]
conversion_id: str
deleted: bool
def __init__(self, conversion_id: _Optional[str] = ..., deleted: _Optional[bool] = ...) -> None: ...
+8
View File
@@ -0,0 +1,8 @@
module github.com/end/officeconvert
go 1.25.0
require (
connectrpc.com/connect v1.19.1
google.golang.org/protobuf v1.36.11
)
+6
View File
@@ -0,0 +1,6 @@
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+108
View File
@@ -0,0 +1,108 @@
syntax = "proto3";
package officeconvertapi.v1;
import "google/protobuf/timestamp.proto";
option go_package = "github.com/end/officeconvert/gen/go/officeconvertapi/v1;officeconvertapiv1";
// ConversionService orchestrates presentation conversion jobs.
service ConversionService {
// CreateConversion allocates a short-lived session and upload URL for a PPTX.
rpc CreateConversion(CreateConversionRequest) returns (CreateConversionResponse) {}
// StartConversion marks upload completion and starts server-side conversion.
rpc StartConversion(StartConversionRequest) returns (StartConversionResponse) {}
// GetConversionStatus returns state transitions for a conversion session.
rpc GetConversionStatus(GetConversionStatusRequest) returns (GetConversionStatusResponse) {}
// GetSlideDeck fetches the final slide deck data after successful conversion.
rpc GetSlideDeck(GetSlideDeckRequest) returns (GetSlideDeckResponse) {}
// DeleteConversion deletes session resources before automatic expiration.
rpc DeleteConversion(DeleteConversionRequest) returns (DeleteConversionResponse) {}
}
// ConversionStatus represents the lifecycle state of a conversion request.
enum ConversionStatus {
CONVERSION_STATUS_UNSPECIFIED = 0;
CONVERSION_STATUS_PENDING = 1;
CONVERSION_STATUS_RUNNING = 2;
CONVERSION_STATUS_SUCCEEDED = 3;
CONVERSION_STATUS_FAILED = 4;
}
// Slide contains extracted notes and the rendered image URL for one slide.
message Slide {
int32 index = 1;
string notes_plain = 2;
string image_url = 3;
}
// SlideDeck is the final structured conversion artifact.
message SlideDeck {
string conversion_id = 1;
string source_filename = 2;
repeated Slide slides = 3;
google.protobuf.Timestamp created_at = 4;
}
// CreateConversionRequest starts a conversion session.
message CreateConversionRequest {
string source_filename = 1;
}
// CreateConversionResponse returns upload details for the session.
message CreateConversionResponse {
string conversion_id = 1;
string upload_bucket = 2;
string upload_object_key = 3;
string upload_url = 4;
google.protobuf.Timestamp expires_at = 5;
}
// StartConversionRequest requests conversion of an already uploaded PPTX.
message StartConversionRequest {
string conversion_id = 1;
}
// StartConversionResponse returns the first known status after enqueue.
message StartConversionResponse {
string conversion_id = 1;
ConversionStatus status = 2;
}
// GetConversionStatusRequest asks for a specific conversion status.
message GetConversionStatusRequest {
string conversion_id = 1;
}
// GetConversionStatusResponse returns current status and optional error info.
message GetConversionStatusResponse {
string conversion_id = 1;
ConversionStatus status = 2;
string error_message = 3;
google.protobuf.Timestamp updated_at = 4;
}
// GetSlideDeckRequest fetches a completed deck.
message GetSlideDeckRequest {
string conversion_id = 1;
}
// GetSlideDeckResponse contains the converted slide deck.
message GetSlideDeckResponse {
SlideDeck slide_deck = 1;
}
// DeleteConversionRequest requests immediate cleanup.
message DeleteConversionRequest {
string conversion_id = 1;
}
// DeleteConversionResponse confirms cleanup details.
message DeleteConversionResponse {
string conversion_id = 1;
bool deleted = 2;
}
@@ -0,0 +1,16 @@
[project]
name = "officeconvert"
version = "0.1.0"
description = "Core conversion primitives for PPTX to SlideDeck artifacts."
readme = "../../../README.md"
requires-python = ">=3.12"
dependencies = [
"python-pptx>=1.0.2",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/officeconvert"]
@@ -0,0 +1,19 @@
"""Public conversion APIs for the officeconvert Python library."""
from officeconvert.conversion import (
SlideArtifact,
SlideDeckResult,
convert_pptx_to_pdf,
convert_pptx_to_slidedeck,
extract_slide_notes,
render_pdf_to_images,
)
__all__ = [
"SlideArtifact",
"SlideDeckResult",
"convert_pptx_to_pdf",
"convert_pptx_to_slidedeck",
"extract_slide_notes",
"render_pdf_to_images",
]
@@ -0,0 +1,225 @@
"""Conversion utilities for transforming PPTX files into slide image artifacts."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import subprocess
from typing import Iterable
from pptx import Presentation
@dataclass(frozen=True, slots=True)
class SlideArtifact:
"""Represents one converted slide image and its extracted notes."""
index: int
image_path: Path
notes_plain: str
@dataclass(frozen=True, slots=True)
class SlideDeckResult:
"""Represents all conversion artifacts for a single source presentation."""
source_filename: str
slides: list[SlideArtifact]
def convert_pptx_to_pdf(pptx_path: Path, pdf_path: Path, *, timeout_s: int = 120) -> Path:
"""Convert a PPTX file to PDF using headless LibreOffice.
Args:
pptx_path: Source `.pptx` path.
pdf_path: Destination `.pdf` path.
timeout_s: Maximum process runtime in seconds.
Returns:
The resolved PDF path.
Raises:
FileNotFoundError: If the source PPTX does not exist.
RuntimeError: If LibreOffice fails or does not create expected output.
"""
if not pptx_path.exists():
raise FileNotFoundError(f"source PPTX does not exist: {pptx_path}")
output_dir = pdf_path.parent.resolve()
output_dir.mkdir(parents=True, exist_ok=True)
command = [
"soffice",
"--headless",
"--convert-to",
"pdf",
"--outdir",
str(output_dir),
str(pptx_path.resolve()),
]
completed = subprocess.run(
command,
check=False,
capture_output=True,
text=True,
timeout=timeout_s,
)
if completed.returncode != 0:
raise RuntimeError(
f"LibreOffice conversion failed: {completed.stderr.strip() or completed.stdout.strip()}"
)
generated_pdf = output_dir / f"{pptx_path.stem}.pdf"
if not generated_pdf.exists():
raise RuntimeError(f"LibreOffice did not create expected PDF: {generated_pdf}")
if generated_pdf != pdf_path:
generated_pdf.replace(pdf_path)
return pdf_path.resolve()
def render_pdf_to_images(
pdf_path: Path,
out_dir: Path,
*,
dpi: int = 180,
image_format: str = "png",
timeout_s: int = 120,
) -> list[Path]:
"""Render each PDF page into an image using Poppler's `pdftoppm`.
Args:
pdf_path: Source PDF path.
out_dir: Output directory for rendered images.
dpi: Target rasterization DPI.
image_format: Image format supported by `pdftoppm` (`png`, `jpeg`, ...).
timeout_s: Maximum command runtime in seconds.
Returns:
Ordered list of slide image paths.
Raises:
FileNotFoundError: If the PDF path does not exist.
RuntimeError: If rasterization fails or no output images are produced.
"""
if not pdf_path.exists():
raise FileNotFoundError(f"source PDF does not exist: {pdf_path}")
out_dir.mkdir(parents=True, exist_ok=True)
prefix_path = out_dir / "slide"
command = [
"pdftoppm",
"-r",
str(dpi),
f"-{image_format}",
str(pdf_path.resolve()),
str(prefix_path),
]
completed = subprocess.run(
command,
check=False,
capture_output=True,
text=True,
timeout=timeout_s,
)
if completed.returncode != 0:
raise RuntimeError(
f"Poppler rasterization failed: {completed.stderr.strip() or completed.stdout.strip()}"
)
images = sorted(out_dir.glob(f"slide-*.{image_format}"))
if not images:
raise RuntimeError(f"no rendered images found in {out_dir}")
return [image.resolve() for image in images]
def extract_slide_notes(pptx_path: Path) -> list[str]:
"""Extract plain-text notes for each slide in slide index order.
Args:
pptx_path: Source presentation path.
Returns:
A list of note strings aligned with source slide order.
Raises:
FileNotFoundError: If the source PPTX does not exist.
"""
if not pptx_path.exists():
raise FileNotFoundError(f"source PPTX does not exist: {pptx_path}")
presentation = Presentation(str(pptx_path.resolve()))
notes: list[str] = []
for slide in presentation.slides:
if not slide.has_notes_slide:
notes.append("")
continue
notes.append(_extract_notes_text(slide.notes_slide.shapes))
return notes
def convert_pptx_to_slidedeck(
pptx_path: Path,
work_dir: Path,
*,
dpi: int = 180,
image_format: str = "png",
) -> SlideDeckResult:
"""Convert a PPTX into rendered images and extracted notes.
The pipeline performs PPTX->PDF conversion with LibreOffice and then PDF->images
rendering with Poppler. Notes are extracted from the original PPTX so text
fidelity is preserved independent of rendering output.
Args:
pptx_path: Source `.pptx` path.
work_dir: Scratch directory for generated outputs.
dpi: Rasterization DPI for output slide images.
image_format: Output image format accepted by `pdftoppm`.
Returns:
Fully materialized `SlideDeckResult` with local image paths.
Raises:
ValueError: If rendered page count differs from note count.
"""
work_dir = work_dir.resolve()
work_dir.mkdir(parents=True, exist_ok=True)
pdf_path = work_dir / f"{pptx_path.stem}.pdf"
image_dir = work_dir / "slides"
convert_pptx_to_pdf(pptx_path, pdf_path)
image_paths = render_pdf_to_images(
pdf_path,
image_dir,
dpi=dpi,
image_format=image_format,
)
notes = extract_slide_notes(pptx_path)
if len(image_paths) != len(notes):
raise ValueError(
"rendered slide count does not match note count: "
f"{len(image_paths)} image(s) vs {len(notes)} note entries"
)
slides = [
SlideArtifact(index=index, image_path=image_path, notes_plain=note)
for index, (image_path, note) in enumerate(zip(image_paths, notes), start=1)
]
return SlideDeckResult(source_filename=pptx_path.name, slides=slides)
def _extract_notes_text(shapes: Iterable[object]) -> str:
"""Extract plain text from note shapes while preserving paragraph breaks."""
segments: list[str] = []
for shape in shapes:
text_frame = getattr(shape, "text_frame", None)
if text_frame is None:
continue
# Join paragraph runs because notes often contain formatting splits.
text = "\n".join(paragraph.text for paragraph in text_frame.paragraphs).strip()
if text:
segments.append(text)
return "\n\n".join(segments).strip()
+19
View File
@@ -0,0 +1,19 @@
[project]
name = "officeconvert-server"
version = "0.1.0"
description = "ConnectRPC server orchestrating file conversions with MinIO."
readme = "../../../README.md"
requires-python = ">=3.12"
dependencies = [
"connectrpc>=0.6.0",
"minio>=7.2.18",
"officeconvert",
"uvicorn>=0.35.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/officeconvert_server"]
@@ -0,0 +1,13 @@
"""Public exports for the officeconvert server package."""
from officeconvert_server.app import app, create_app
from officeconvert_server.config import ServerConfig, load_server_config
from officeconvert_server.service import ConversionServiceImpl
__all__ = [
"ServerConfig",
"ConversionServiceImpl",
"app",
"create_app",
"load_server_config",
]
@@ -0,0 +1,27 @@
"""ASGI application entrypoint for the officeconvert Connect service."""
from __future__ import annotations
from officeconvertapi.v1.conversion_connect import ConversionServiceASGIApplication
from officeconvert_server.config import load_server_config
from officeconvert_server.service import ConversionServiceImpl
from officeconvert_server.storage import MinIOStore
def create_app() -> ConversionServiceASGIApplication:
"""Construct and return the configured Connect ASGI application."""
config = load_server_config()
store = MinIOStore(
endpoint=config.minio_endpoint,
access_key=config.minio_access_key,
secret_key=config.minio_secret_key,
secure=config.minio_secure,
public_endpoint=config.minio_public_endpoint,
)
service = ConversionServiceImpl(config=config, store=store)
return ConversionServiceASGIApplication(service)
# Exported ASGI application for `uvicorn officeconvert_server.app:app`.
app = create_app()
@@ -0,0 +1,34 @@
"""Runtime configuration for the officeconvert Connect server."""
from __future__ import annotations
from dataclasses import dataclass
import os
@dataclass(frozen=True, slots=True)
class ServerConfig:
"""Defines environment-driven settings for server orchestration."""
minio_endpoint: str
minio_access_key: str
minio_secret_key: str
minio_secure: bool
minio_public_endpoint: str
minio_session_ttl_seconds: int
conversion_cleanup_delay_seconds: int
def load_server_config() -> ServerConfig:
"""Load server configuration from environment variables."""
return ServerConfig(
minio_endpoint=os.getenv("MINIO_ENDPOINT", "localhost:9000"),
minio_access_key=os.getenv("MINIO_ACCESS_KEY", "minioadmin"),
minio_secret_key=os.getenv("MINIO_SECRET_KEY", "minioadmin"),
minio_secure=os.getenv("MINIO_USE_SSL", "false").lower() == "true",
minio_public_endpoint=os.getenv("MINIO_PUBLIC_ENDPOINT", "localhost:9000"),
minio_session_ttl_seconds=int(os.getenv("MINIO_SESSION_TTL_SECONDS", "3600")),
conversion_cleanup_delay_seconds=int(
os.getenv("CONVERSION_CLEANUP_DELAY_SECONDS", "3600")
),
)
@@ -0,0 +1,30 @@
"""In-memory models representing conversion workflow state."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def utc_now() -> datetime:
"""Return the current UTC timestamp with timezone information."""
return datetime.now(tz=timezone.utc)
@dataclass(slots=True)
class ConversionSession:
"""Stores mutable state for a single conversion lifecycle."""
conversion_id: str
source_filename: str
bucket_name: str
upload_object_key: str
status: int
created_at: datetime = field(default_factory=utc_now)
updated_at: datetime = field(default_factory=utc_now)
error_message: str = ""
slide_deck: Any | None = None
work_dir: Path | None = None
conversion_task: Any | None = None
cleanup_task: Any | None = None
@@ -0,0 +1,269 @@
"""Connect service implementation for conversion request orchestration."""
from __future__ import annotations
import asyncio
from datetime import datetime, timedelta, timezone
from pathlib import Path
import shutil
import tempfile
import uuid
from connectrpc.code import Code
from connectrpc.errors import ConnectError
from connectrpc.request import RequestContext
from google.protobuf.timestamp_pb2 import Timestamp
from officeconvert import SlideArtifact, convert_pptx_to_slidedeck
from officeconvertapi.v1 import conversion_connect, conversion_pb2
from officeconvert_server.config import ServerConfig
from officeconvert_server.models import ConversionSession, utc_now
from officeconvert_server.storage import MinIOStore
class ConversionServiceImpl(conversion_connect.ConversionService):
"""Implements the conversion API with in-memory state and MinIO orchestration."""
def __init__(self, config: ServerConfig, store: MinIOStore) -> None:
"""Initialize service with runtime config and storage adapter."""
self._config = config
self._store = store
self._sessions: dict[str, ConversionSession] = {}
self._lock = asyncio.Lock()
async def create_conversion(
self,
request: conversion_pb2.CreateConversionRequest,
ctx: RequestContext,
) -> conversion_pb2.CreateConversionResponse:
"""Create a new conversion session and return upload credentials."""
del ctx
source_filename = request.source_filename.strip()
if not source_filename:
raise ConnectError(Code.INVALID_ARGUMENT, "source_filename is required")
if not source_filename.lower().endswith(".pptx"):
raise ConnectError(Code.INVALID_ARGUMENT, "only .pptx input is supported")
conversion_id = str(uuid.uuid4())
bucket_name = f"oc-{conversion_id}"
upload_key = "input/source.pptx"
expires_at = utc_now() + timedelta(seconds=self._config.minio_session_ttl_seconds)
self._store.ensure_bucket(bucket_name)
upload_url = self._store.presigned_put_url(
bucket_name,
upload_key,
ttl_seconds=self._config.minio_session_ttl_seconds,
)
session = ConversionSession(
conversion_id=conversion_id,
source_filename=source_filename,
bucket_name=bucket_name,
upload_object_key=upload_key,
status=conversion_pb2.CONVERSION_STATUS_PENDING,
)
async with self._lock:
self._sessions[conversion_id] = session
return conversion_pb2.CreateConversionResponse(
conversion_id=conversion_id,
upload_bucket=bucket_name,
upload_object_key=upload_key,
upload_url=upload_url,
expires_at=_to_timestamp(expires_at),
)
async def start_conversion(
self,
request: conversion_pb2.StartConversionRequest,
ctx: RequestContext,
) -> conversion_pb2.StartConversionResponse:
"""Start asynchronous conversion for an already-uploaded session payload."""
del ctx
session = await self._get_session(request.conversion_id)
async with self._lock:
if session.status == conversion_pb2.CONVERSION_STATUS_RUNNING:
return conversion_pb2.StartConversionResponse(
conversion_id=session.conversion_id,
status=session.status,
)
if session.status in (
conversion_pb2.CONVERSION_STATUS_FAILED,
conversion_pb2.CONVERSION_STATUS_SUCCEEDED,
):
raise ConnectError(
Code.FAILED_PRECONDITION,
"conversion has already completed",
)
session.status = conversion_pb2.CONVERSION_STATUS_RUNNING
session.updated_at = utc_now()
session.conversion_task = asyncio.create_task(self._run_conversion(session))
return conversion_pb2.StartConversionResponse(
conversion_id=session.conversion_id,
status=session.status,
)
async def get_conversion_status(
self,
request: conversion_pb2.GetConversionStatusRequest,
ctx: RequestContext,
) -> conversion_pb2.GetConversionStatusResponse:
"""Return current conversion status and optional error details."""
del ctx
session = await self._get_session(request.conversion_id)
return conversion_pb2.GetConversionStatusResponse(
conversion_id=session.conversion_id,
status=session.status,
error_message=session.error_message,
updated_at=_to_timestamp(session.updated_at),
)
async def get_slide_deck(
self,
request: conversion_pb2.GetSlideDeckRequest,
ctx: RequestContext,
) -> conversion_pb2.GetSlideDeckResponse:
"""Return the finished slide deck once conversion succeeds."""
del ctx
session = await self._get_session(request.conversion_id)
if session.status == conversion_pb2.CONVERSION_STATUS_FAILED:
raise ConnectError(Code.FAILED_PRECONDITION, session.error_message)
if session.status != conversion_pb2.CONVERSION_STATUS_SUCCEEDED:
raise ConnectError(Code.FAILED_PRECONDITION, "conversion is not finished yet")
if session.slide_deck is None:
raise ConnectError(Code.INTERNAL, "slide deck missing from successful session")
return conversion_pb2.GetSlideDeckResponse(slide_deck=session.slide_deck)
async def delete_conversion(
self,
request: conversion_pb2.DeleteConversionRequest,
ctx: RequestContext,
) -> conversion_pb2.DeleteConversionResponse:
"""Delete a conversion session and associated MinIO/local artifacts."""
del ctx
async with self._lock:
session = self._sessions.pop(request.conversion_id, None)
if session is None:
return conversion_pb2.DeleteConversionResponse(
conversion_id=request.conversion_id,
deleted=False,
)
if session.cleanup_task is not None:
session.cleanup_task.cancel()
if session.conversion_task is not None and not session.conversion_task.done():
session.conversion_task.cancel()
await self._cleanup_local_artifacts(session)
await asyncio.to_thread(self._store.remove_bucket_tree, session.bucket_name)
return conversion_pb2.DeleteConversionResponse(
conversion_id=session.conversion_id,
deleted=True,
)
async def _run_conversion(self, session: ConversionSession) -> None:
"""Execute conversion flow and persist terminal state in memory."""
work_dir = Path(
tempfile.mkdtemp(prefix=f"officeconvert-{session.conversion_id}-")
).resolve()
session.work_dir = work_dir
source_path = work_dir / "input.pptx"
try:
await asyncio.to_thread(
self._store.fget_object,
session.bucket_name,
session.upload_object_key,
source_path,
)
result = await asyncio.to_thread(
convert_pptx_to_slidedeck,
source_path,
work_dir,
)
session.slide_deck = await asyncio.to_thread(
self._upload_and_build_slide_deck,
session,
result.slides,
result.source_filename,
)
session.status = conversion_pb2.CONVERSION_STATUS_SUCCEEDED
session.updated_at = utc_now()
except asyncio.CancelledError:
session.status = conversion_pb2.CONVERSION_STATUS_FAILED
session.error_message = "conversion cancelled"
session.updated_at = utc_now()
raise
except Exception as exc:
session.status = conversion_pb2.CONVERSION_STATUS_FAILED
session.error_message = str(exc)
session.updated_at = utc_now()
finally:
await self._cleanup_local_artifacts(session)
session.cleanup_task = asyncio.create_task(self._delayed_cleanup(session))
def _upload_and_build_slide_deck(
self,
session: ConversionSession,
slides: list[SlideArtifact],
source_filename: str,
) -> conversion_pb2.SlideDeck:
"""Upload generated slide images and construct API response payload."""
response_slides: list[conversion_pb2.Slide] = []
for slide in slides:
object_key = f"output/slide-{slide.index:04d}{slide.image_path.suffix}"
self._store.fput_object(session.bucket_name, object_key, slide.image_path)
image_url = self._store.presigned_get_url(
session.bucket_name,
object_key,
ttl_seconds=self._config.minio_session_ttl_seconds,
)
response_slides.append(
conversion_pb2.Slide(
index=slide.index,
notes_plain=slide.notes_plain,
image_url=image_url,
)
)
return conversion_pb2.SlideDeck(
conversion_id=session.conversion_id,
source_filename=source_filename,
slides=response_slides,
created_at=_to_timestamp(utc_now()),
)
async def _delayed_cleanup(self, session: ConversionSession) -> None:
"""Delete storage resources after the configured session retention period."""
try:
await asyncio.sleep(self._config.conversion_cleanup_delay_seconds)
await asyncio.to_thread(self._store.remove_bucket_tree, session.bucket_name)
except asyncio.CancelledError:
return
finally:
async with self._lock:
self._sessions.pop(session.conversion_id, None)
async def _cleanup_local_artifacts(self, session: ConversionSession) -> None:
"""Delete temporary local files for a session if they still exist."""
if session.work_dir is not None and session.work_dir.exists():
await asyncio.to_thread(shutil.rmtree, session.work_dir, True)
session.work_dir = None
async def _get_session(self, conversion_id: str) -> ConversionSession:
"""Return an existing session or raise a NOT_FOUND error."""
async with self._lock:
session = self._sessions.get(conversion_id)
if session is None:
raise ConnectError(Code.NOT_FOUND, "conversion_id not found")
return session
def _to_timestamp(value: datetime) -> Timestamp:
"""Convert a timezone-aware datetime to protobuf Timestamp."""
normalized = value.astimezone(timezone.utc)
proto = Timestamp()
proto.FromDatetime(normalized)
return proto
@@ -0,0 +1,94 @@
"""MinIO helper abstraction for upload and artifact lifecycle."""
from __future__ import annotations
from datetime import timedelta
from pathlib import Path
from urllib.parse import urlparse
from minio import Minio
from minio.deleteobjects import DeleteObject
from minio.error import S3Error
class MinIOStore:
"""Provides typed helper methods around MinIO object storage operations."""
def __init__(
self,
*,
endpoint: str,
access_key: str,
secret_key: str,
secure: bool,
public_endpoint: str,
) -> None:
"""Initialize MinIO clients for internal and public URL generation."""
self._client = Minio(
endpoint,
access_key=access_key,
secret_key=secret_key,
secure=secure,
)
self._public_client = Minio(
public_endpoint,
access_key=access_key,
secret_key=secret_key,
secure=secure,
)
def ensure_bucket(self, bucket_name: str) -> None:
"""Create a bucket if it does not already exist."""
if not self._client.bucket_exists(bucket_name):
self._client.make_bucket(bucket_name)
def presigned_put_url(self, bucket_name: str, object_key: str, *, ttl_seconds: int) -> str:
"""Generate a presigned PUT URL for a single object upload."""
return self._public_client.presigned_put_object(
bucket_name,
object_key,
expires=timedelta(seconds=ttl_seconds),
)
def presigned_get_url(self, bucket_name: str, object_key: str, *, ttl_seconds: int) -> str:
"""Generate a presigned GET URL for downloading one object."""
return self._public_client.presigned_get_object(
bucket_name,
object_key,
expires=timedelta(seconds=ttl_seconds),
)
def fget_object(self, bucket_name: str, object_key: str, output_path: Path) -> None:
"""Download one object from MinIO to a local filesystem path."""
output_path.parent.mkdir(parents=True, exist_ok=True)
self._client.fget_object(bucket_name, object_key, str(output_path))
def fput_object(self, bucket_name: str, object_key: str, source_path: Path) -> None:
"""Upload one local filesystem object to MinIO."""
self._client.fput_object(bucket_name, object_key, str(source_path))
def remove_bucket_tree(self, bucket_name: str) -> None:
"""Remove all objects in a bucket and then delete the bucket."""
objects = list(self._client.list_objects(bucket_name, recursive=True))
if objects:
errors = self._client.remove_objects(
bucket_name,
[DeleteObject(obj.object_name) for obj in objects],
)
for err in errors:
raise RuntimeError(
f"failed to delete object {err.object_name}: {err.message}"
)
try:
self._client.remove_bucket(bucket_name)
except S3Error as exc:
# Concurrent cleanup paths may race to remove the same bucket.
if exc.code != "NoSuchBucket":
raise
def object_key_from_presigned_url(url: str) -> str:
"""Extract object key from a presigned URL path for diagnostics."""
path = urlparse(url).path
path_parts = [part for part in path.split("/") if part]
return "/".join(path_parts[1:]) if len(path_parts) >= 2 else ""
+11
View File
@@ -0,0 +1,11 @@
[project]
name = "officeconvert-workspace"
version = "0.1.0"
description = "Workspace root for officeconvert Python packages."
requires-python = ">=3.12"
[tool.uv.workspace]
members = [
"packages/officeconvert",
"packages/server",
]