diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..707bbf6 --- /dev/null +++ b/.env.example @@ -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 diff --git a/Dockerfile.server b/Dockerfile.server new file mode 100644 index 0000000..e54b1ab --- /dev/null +++ b/Dockerfile.server @@ -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"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2bc457e --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index efc8c94..2ac9e29 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..9fbdb65 --- /dev/null +++ b/buf.gen.yaml @@ -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 diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..c7e30e3 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: proto +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/clients/go/go.mod b/clients/go/go.mod new file mode 100644 index 0000000..d3f4c0d --- /dev/null +++ b/clients/go/go.mod @@ -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 => ../.. diff --git a/clients/go/go.sum b/clients/go/go.sum new file mode 100644 index 0000000..6dfa443 --- /dev/null +++ b/clients/go/go.sum @@ -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= diff --git a/clients/go/officeconvertclient/client.go b/clients/go/officeconvertclient/client.go new file mode 100644 index 0000000..06db30e --- /dev/null +++ b/clients/go/officeconvertclient/client.go @@ -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: + } + } +} diff --git a/clients/go/officeconvertclient/download.go b/clients/go/officeconvertclient/download.go new file mode 100644 index 0000000..84bc7be --- /dev/null +++ b/clients/go/officeconvertclient/download.go @@ -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" + } +} diff --git a/clients/go/officeconvertclient/orchestrate.go b/clients/go/officeconvertclient/orchestrate.go new file mode 100644 index 0000000..b605114 --- /dev/null +++ b/clients/go/officeconvertclient/orchestrate.go @@ -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 +} diff --git a/clients/go/officeconvertclient/upload.go b/clients/go/officeconvertclient/upload.go new file mode 100644 index 0000000..9a646a0 --- /dev/null +++ b/clients/go/officeconvertclient/upload.go @@ -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 +} diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml new file mode 100644 index 0000000..16c4a7b --- /dev/null +++ b/deploy/docker-compose.dev.yml @@ -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: diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..d64cf76 --- /dev/null +++ b/deploy/docker-compose.yml @@ -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: diff --git a/gen/go/officeconvertapi/v1/conversion.pb.go b/gen/go/officeconvertapi/v1/conversion.pb.go new file mode 100644 index 0000000..3d222da --- /dev/null +++ b/gen/go/officeconvertapi/v1/conversion.pb.go @@ -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 +} diff --git a/gen/go/officeconvertapi/v1/officeconvertapiv1connect/conversion.connect.go b/gen/go/officeconvertapi/v1/officeconvertapiv1connect/conversion.connect.go new file mode 100644 index 0000000..ad1960f --- /dev/null +++ b/gen/go/officeconvertapi/v1/officeconvertapiv1connect/conversion.connect.go @@ -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")) +} diff --git a/gen/python/officeconvertapi/v1/conversion_connect.py b/gen/python/officeconvertapi/v1/conversion_connect.py new file mode 100644 index 0000000..e36144f --- /dev/null +++ b/gen/python/officeconvertapi/v1/conversion_connect.py @@ -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, + ) diff --git a/gen/python/officeconvertapi/v1/conversion_pb2.py b/gen/python/officeconvertapi/v1/conversion_pb2.py new file mode 100644 index 0000000..6f76af6 --- /dev/null +++ b/gen/python/officeconvertapi/v1/conversion_pb2.py @@ -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) diff --git a/gen/python/officeconvertapi/v1/conversion_pb2.pyi b/gen/python/officeconvertapi/v1/conversion_pb2.pyi new file mode 100644 index 0000000..877b479 --- /dev/null +++ b/gen/python/officeconvertapi/v1/conversion_pb2.pyi @@ -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: ... diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ae9fdd5 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6dfa443 --- /dev/null +++ b/go.sum @@ -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= diff --git a/proto/officeconvertapi/v1/conversion.proto b/proto/officeconvertapi/v1/conversion.proto new file mode 100644 index 0000000..a2e4cc0 --- /dev/null +++ b/proto/officeconvertapi/v1/conversion.proto @@ -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; +} diff --git a/python/packages/officeconvert/pyproject.toml b/python/packages/officeconvert/pyproject.toml new file mode 100644 index 0000000..4ced3b5 --- /dev/null +++ b/python/packages/officeconvert/pyproject.toml @@ -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"] diff --git a/python/packages/officeconvert/src/officeconvert/__init__.py b/python/packages/officeconvert/src/officeconvert/__init__.py new file mode 100644 index 0000000..8375318 --- /dev/null +++ b/python/packages/officeconvert/src/officeconvert/__init__.py @@ -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", +] diff --git a/python/packages/officeconvert/src/officeconvert/conversion.py b/python/packages/officeconvert/src/officeconvert/conversion.py new file mode 100644 index 0000000..a107f32 --- /dev/null +++ b/python/packages/officeconvert/src/officeconvert/conversion.py @@ -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() diff --git a/python/packages/server/pyproject.toml b/python/packages/server/pyproject.toml new file mode 100644 index 0000000..a5c47ee --- /dev/null +++ b/python/packages/server/pyproject.toml @@ -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"] diff --git a/python/packages/server/src/officeconvert_server/__init__.py b/python/packages/server/src/officeconvert_server/__init__.py new file mode 100644 index 0000000..406901d --- /dev/null +++ b/python/packages/server/src/officeconvert_server/__init__.py @@ -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", +] diff --git a/python/packages/server/src/officeconvert_server/app.py b/python/packages/server/src/officeconvert_server/app.py new file mode 100644 index 0000000..51f8b32 --- /dev/null +++ b/python/packages/server/src/officeconvert_server/app.py @@ -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() diff --git a/python/packages/server/src/officeconvert_server/config.py b/python/packages/server/src/officeconvert_server/config.py new file mode 100644 index 0000000..c9d56c5 --- /dev/null +++ b/python/packages/server/src/officeconvert_server/config.py @@ -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") + ), + ) diff --git a/python/packages/server/src/officeconvert_server/models.py b/python/packages/server/src/officeconvert_server/models.py new file mode 100644 index 0000000..bd6dc4c --- /dev/null +++ b/python/packages/server/src/officeconvert_server/models.py @@ -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 diff --git a/python/packages/server/src/officeconvert_server/service.py b/python/packages/server/src/officeconvert_server/service.py new file mode 100644 index 0000000..daaeb5d --- /dev/null +++ b/python/packages/server/src/officeconvert_server/service.py @@ -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 diff --git a/python/packages/server/src/officeconvert_server/storage.py b/python/packages/server/src/officeconvert_server/storage.py new file mode 100644 index 0000000..2f0a940 --- /dev/null +++ b/python/packages/server/src/officeconvert_server/storage.py @@ -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 "" diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..dfdca62 --- /dev/null +++ b/python/pyproject.toml @@ -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", +]