{"id":38125500,"url":"https://github.com/patent-dev/epo-ops","last_synced_at":"2026-01-16T22:33:26.211Z","repository":{"id":320685456,"uuid":"1082012323","full_name":"patent-dev/epo-ops","owner":"patent-dev","description":"A Go client library for the European Patent Office's Open Patent Services (OPS) API v3.2.","archived":false,"fork":false,"pushed_at":"2025-10-25T07:19:18.000Z","size":4064,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-25T09:13:58.213Z","etag":null,"topics":["api","patent-data","patent-data-api","patents"],"latest_commit_sha":null,"homepage":"https://patent.dev","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/patent-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-23T15:53:44.000Z","updated_at":"2025-10-25T07:19:21.000Z","dependencies_parsed_at":"2025-10-25T09:14:45.381Z","dependency_job_id":"1c632ba9-c501-411d-813e-a87aa3f5dfeb","html_url":"https://github.com/patent-dev/epo-ops","commit_stats":null,"previous_names":["patent-dev/epo-ops"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/patent-dev/epo-ops","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patent-dev%2Fepo-ops","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patent-dev%2Fepo-ops/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patent-dev%2Fepo-ops/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patent-dev%2Fepo-ops/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/patent-dev","download_url":"https://codeload.github.com/patent-dev/epo-ops/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patent-dev%2Fepo-ops/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28485373,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T11:59:17.896Z","status":"ssl_error","status_checked_at":"2026-01-16T11:55:55.838Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","patent-data","patent-data-api","patents"],"created_at":"2026-01-16T22:33:26.118Z","updated_at":"2026-01-16T22:33:26.195Z","avatar_url":"https://github.com/patent-dev.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EPO OPS Go Client\n\nA Go client library for the European Patent Office's Open Patent Services (OPS) API v3.2.\n\n## Overview\n\nThis library provides an idiomatic Go interface to interact with the EPO's Open Patent Services, allowing you to:\n\n- Retrieve patent bibliographic data, claims, descriptions, and abstracts\n- Search for patents using various criteria\n- Get patent family information (INPADOC)\n- Access CPC/ECLA classification data (schema, statistics, mapping, media)\n- Download patent images and convert TIFF to PNG\n- Access legal status and register data\n- Track API quota usage\n\n## Features\n\n- OAuth2 authentication with automatic token management\n- Patent text retrieval (biblio, claims, description, abstract, fulltext)\n- Patent search using CQL (Contextual Query Language)\n- INPADOC family retrieval\n- CPC/ECLA classification services (schema, statistics, mapping, media)\n- Patent image retrieval with TIFF to PNG conversion\n- Legal status retrieval\n- EPO Register access (biblio, events, procedural steps, unitary patent)\n- Patent number format conversion\n- Comprehensive error handling with custom error types\n- Automatic retry logic with exponential backoff\n- Quota tracking and fair use monitoring\n- Unit and integration tests\n\n\n## Installation\n\n```bash\ngo get github.com/patent-dev/epo-ops\n```\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n\n    ops \"github.com/patent-dev/epo-ops\"\n)\n\nfunc main() {\n    config := \u0026ops.Config{\n        ConsumerKey:    \"your-consumer-key\",\n        ConsumerSecret: \"your-consumer-secret\",\n    }\n\n    client, err := ops.NewClient(config)\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    ctx := context.Background()\n\n    // Retrieve bibliographic data\n    biblio, err := client.GetBiblio(ctx, \"publication\", \"docdb\", \"EP1000000\")\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Println(\"Biblio:\", biblio)\n\n    // Search patents\n    results, err := client.Search(ctx, \"ti=plastic\", \"1-5\")\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Println(\"Search results:\", results)\n\n    // Get patent family\n    family, err := client.GetFamily(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Println(\"Family:\", family)\n\n    // Get patent image (first page of drawings)\n    imageData, err := client.GetImage(ctx, \"EP\", \"1000000\", \"B1\", \"Drawing\", 1)\n    if err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"Retrieved image: %d bytes\\n\", len(imageData))\n}\n```\n\n## Parsed vs Raw API\n\nThis library provides two ways to access API data:\n\n### Parsed API (Type-Safe, Recommended)\n\nBy default, methods return parsed Go structs for type-safe access:\n\n```go\n// Returns *BibliographicData struct\nbiblio, err := client.GetBiblio(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nfmt.Printf(\"Title: %s\\n\", biblio.InventionTitle)\nfmt.Printf(\"Applicants: %d\\n\", len(biblio.Applicants))\n\n// Returns *FamilyData struct\nfamily, err := client.GetFamily(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nfmt.Printf(\"Family ID: %s\\n\", family.FamilyID)\nfmt.Printf(\"Members: %d\\n\", len(family.Members))\n\n// Returns *SearchResultData struct\nresults, err := client.Search(ctx, \"ti=battery\", \"1-5\")\nfmt.Printf(\"Total: %d\\n\", results.TotalResults)\nfor _, patent := range results.Patents {\n    fmt.Printf(\"  %s\\n\", patent.Number)\n}\n\n// Returns *LegalData struct\nlegal, err := client.GetLegal(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nfor _, event := range legal.LegalEvents {\n    fmt.Printf(\"%s: %s\\n\", event.Date, event.Code)\n}\n```\n\n### Raw API (XML Access)\n\nFor special cases (e.g., saving raw XML, custom parsing), use `*Raw()` methods:\n\n```go\n// Returns raw XML string\nxmlData, err := client.GetBiblioRaw(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nos.WriteFile(\"biblio.xml\", []byte(xmlData), 0644)\n\n// All endpoints have Raw variants\nfamilyXML, err := client.GetFamilyRaw(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nsearchXML, err := client.SearchRaw(ctx, \"ti=battery\", \"1-5\")\nlegalXML, err := client.GetLegalRaw(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\n```\n\n**Architecture Note**: Parsed methods internally call the corresponding `*Raw()` method and parse the result. This ensures consistent data access and eliminates code duplication.\n\n## Getting Credentials\n\nTo use the EPO OPS API, you need to register for API credentials:\n\n1. Visit https://developers.epo.org/\n2. Create an account or sign in\n3. Register a new application to get your consumer key and secret\n\n## Fair Use Policy \u0026 Quota Tracking\n\nThe EPO OPS API has usage limits:\n- **Non-paying users**: 4 GB/week (free)\n- **Paying users**: \u003e4 GB/week (€2,800/year)\n\nSee: https://www.epo.org/en/service-support/ordering/fair-use\n\nThis client automatically tracks quota usage from API responses:\n\n```go\n// Make API calls\nclient.GetBiblio(ctx, \"publication\", \"docdb\", \"EP1000000\")\n\n// Check quota status\nquota := client.GetLastQuota()\nif quota != nil {\n    fmt.Printf(\"Status: %s\\n\", quota.Status) // \"green\", \"yellow\", \"red\", or \"black\"\n    fmt.Printf(\"Individual: %d/%d (%.2f%%)\\n\",\n        quota.Individual.Used,\n        quota.Individual.Limit,\n        quota.Individual.UsagePercent())\n}\n```\n\n## Image Retrieval \u0026 TIFF Conversion\n\nPatent images from EPO are typically in TIFF format. This library includes utilities to convert TIFF to PNG:\n\n```go\nimport (\n    ops \"github.com/patent-dev/epo-ops\"\n    \"github.com/patent-dev/epo-ops/tiffutil\"\n)\n\n// Retrieve patent image (TIFF format)\nimageData, err := client.GetImage(ctx, \"EP\", \"1000000\", \"B1\", \"Drawing\", 1)\nif err != nil {\n    log.Fatal(err)\n}\n\n// Convert TIFF to PNG (with automatic rotation for landscape images)\npngData, err := tiffutil.TIFFToPNG(imageData)\nif err != nil {\n    log.Fatal(err)\n}\n\n// Save PNG file\nos.WriteFile(\"patent_drawing.png\", pngData, 0644)\n\n// Or convert without rotation\npngData, err := tiffutil.TIFFToPNGNoRotate(imageData)\n\n// Or batch convert multiple pages\npngImages, err := tiffutil.BatchTIFFToPNG([][]byte{imageData1, imageData2, imageData3})\n```\n\nThe TIFF utilities support:\n- CCITT Group 3/4 compression (common in patent drawings)\n- LZW compression\n- CMYK color model\n- Automatic landscape-to-portrait rotation\n\n## API Reference\n\n### Client Creation\n\n```go\n// Create client with default configuration\nconfig := \u0026ops.Config{\n    ConsumerKey:    \"your-key\",\n    ConsumerSecret: \"your-secret\",\n}\nclient, err := ops.NewClient(config)\n\n// Create client with custom configuration\nconfig := \u0026ops.Config{\n    ConsumerKey:    \"your-key\",\n    ConsumerSecret: \"your-secret\",\n    BaseURL:        \"https://ops.epo.org/3.2/rest-services\",  // Default\n    MaxRetries:     3,                                          // Default\n    RetryDelay:     time.Second,                                // Default\n    Timeout:        30 * time.Second,                           // Default\n}\nclient, err := ops.NewClient(config)\n```\n\n### Published Data Retrieval\n\nAll methods return parsed Go structs by default. Use `*Raw()` variants for XML access.\n\n```go\n// Retrieve bibliographic data → *BibliographicData\nbiblio, err := client.GetBiblio(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nif err != nil {\n    log.Fatal(err)\n}\nfmt.Printf(\"Title: %s\\n\", biblio.InventionTitle)\nfmt.Printf(\"Publication Date: %s\\n\", biblio.PublicationDate)\nfor _, applicant := range biblio.Applicants {\n    fmt.Printf(\"Applicant: %s\\n\", applicant.Name)\n}\n\n// Retrieve claims → *ClaimsData\nclaims, err := client.GetClaims(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nfmt.Printf(\"Claims count: %d\\n\", len(claims.Claims))\nfor _, claim := range claims.Claims {\n    fmt.Printf(\"Claim %s: %s\\n\", claim.Number, claim.Text)\n}\n\n// Retrieve description → *DescriptionData\ndescription, err := client.GetDescription(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nfmt.Printf(\"Paragraphs: %d\\n\", len(description.Paragraphs))\n\n// Retrieve abstract → *AbstractData\nabstract, err := client.GetAbstract(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nfmt.Printf(\"Abstract: %s\\n\", abstract.Text)\n\n// Retrieve full text → *FulltextData (biblio + abstract + description + claims)\nfulltext, err := client.GetFulltext(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nfmt.Printf(\"Title: %s\\n\", fulltext.Biblio.InventionTitle)\nfmt.Printf(\"Abstract: %s\\n\", fulltext.Abstract.Text)\nfmt.Printf(\"Description paragraphs: %d\\n\", len(fulltext.Description.Paragraphs))\nfmt.Printf(\"Claims: %d\\n\", len(fulltext.Claims.Claims))\n\n// Get published equivalents (simple family) → *EquivalentsData\nequivalents, err := client.GetPublishedEquivalents(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nfmt.Printf(\"Equivalents: %d\\n\", len(equivalents.Equivalents))\nfor _, eq := range equivalents.Equivalents {\n    fmt.Printf(\"  %s (Date: %s, Kind: %s)\\n\", eq.DocNumber, eq.Date, eq.Kind)\n}\n\n// Raw XML access (if needed)\nxmlData, err := client.GetBiblioRaw(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nos.WriteFile(\"biblio.xml\", []byte(xmlData), 0644)\n```\n\n**Parameters**:\n- `refType`: Reference type - `\"publication\"`, `\"application\"`, or `\"priority\"`\n- `format`: Number format - `\"docdb\"` or `\"epodoc\"`\n- `number`: Patent number (e.g., `\"EP1000000B1\"`)\n\n### Search\n\nReturns `*SearchResultData` with parsed results.\n\n```go\n// Basic search → *SearchResultData\nresults, err := client.Search(ctx, \"ti=battery\", \"1-25\")\nif err != nil {\n    log.Fatal(err)\n}\n\nfmt.Printf(\"Total results: %d\\n\", results.TotalResults)\nfmt.Printf(\"Range: %s\\n\", results.Range)\nfmt.Printf(\"Returned: %d patents\\n\", len(results.Patents))\n\nfor _, patent := range results.Patents {\n    fmt.Printf(\"Patent: %s\\n\", patent.Number)\n    fmt.Printf(\"  Country: %s, Date: %s, Kind: %s\\n\",\n        patent.Country, patent.Date, patent.Kind)\n}\n\n// Search with specific constituent → *SearchResultData\nresults, err := client.SearchWithConstituent(ctx, \"biblio\", \"pa=Siemens\", \"1-10\")\n\n// Raw XML access\nxmlData, err := client.SearchRaw(ctx, \"ti=battery\", \"1-25\")\n```\n\n**CQL Query Examples**:\n- `ti=plastic` - Title contains \"plastic\"\n- `pa=Siemens` - Applicant is Siemens\n- `ti=plastic and pa=Siemens` - Combined search\n- `de` - Country code DE\n\n**Range Format**: `\"1-25\"` (default), `\"1-100\"`, etc.\n\n### Family Retrieval\n\nReturns `*FamilyData` with parsed family information.\n\n```go\n// Basic INPADOC family → *FamilyData\nfamily, err := client.GetFamily(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nif err != nil {\n    log.Fatal(err)\n}\n\nfmt.Printf(\"Family ID: %s\\n\", family.FamilyID)\nfmt.Printf(\"Patent Number: %s\\n\", family.PatentNumber)\nfmt.Printf(\"Total Members: %d\\n\", family.TotalCount)\nfmt.Printf(\"Has Legal Data: %v\\n\", family.Legal)\n\nfor _, member := range family.Members {\n    fmt.Printf(\"Member: %s %s %s (Date: %s)\\n\",\n        member.Country, member.DocNumber, member.Kind, member.Date)\n\n    if member.ApplicationRef.DocNumber != \"\" {\n        fmt.Printf(\"  Application: %s %s (Date: %s)\\n\",\n            member.ApplicationRef.Country,\n            member.ApplicationRef.DocNumber,\n            member.ApplicationRef.Date)\n    }\n\n    for _, priority := range member.PriorityClaims {\n        fmt.Printf(\"  Priority: %s %s (Date: %s)\\n\",\n            priority.Country, priority.DocNumber, priority.Date)\n    }\n}\n\n// Family with bibliographic data → *FamilyData\nfamily, err := client.GetFamilyWithBiblio(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\n\n// Family with legal status → *FamilyData\nfamily, err := client.GetFamilyWithLegal(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\n\n// Raw XML access\nxmlData, err := client.GetFamilyRaw(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\n```\n\n### Images\n\n```go\n// Retrieve patent image (typically TIFF format)\nimageData, err := client.GetImage(ctx, \"EP\", \"1000000\", \"B1\", \"Drawing\", 1)\n\n// Image types: \"FullDocument\", \"Drawing\", \"FirstPageClipping\"\n// Page: 1-based page number\n```\n\n### Legal \u0026 Register\n\n```go\n// Legal status data → *LegalData\nlegal, err := client.GetLegal(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nif err != nil {\n    log.Fatal(err)\n}\n\nfmt.Printf(\"Patent: %s\\n\", legal.PatentNumber)\nfmt.Printf(\"Legal Events: %d\\n\", len(legal.LegalEvents))\n\nfor _, event := range legal.LegalEvents {\n    fmt.Printf(\"Event: %s (Code: %s)\\n\", event.Date, event.Code)\n    fmt.Printf(\"  Country: %s\\n\", event.Country)\n    if event.Description != \"\" {\n        fmt.Printf(\"  Description: %s\\n\", event.Description)\n    }\n    if event.Status != \"\" {\n        fmt.Printf(\"  Status: %s\\n\", event.Status)\n    }\n}\n\n// Raw XML access\nxmlData, err := client.GetLegalRaw(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\n\n// EPO Register bibliographic data (returns raw XML)\nregisterBiblio, err := client.GetRegisterBiblioRaw(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\n\n// EPO Register procedural events (returns raw XML)\nevents, err := client.GetRegisterEventsRaw(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\n```\n\n### Number Conversion\n\n```go\n// Convert patent number formats\nconverted, err := client.ConvertPatentNumber(ctx, \"publication\", \"docdb\", \"EP1000000B1\", \"epodoc\")\n```\n\n**Formats**:\n- `original`: `US.(05/948,554).19781004`\n- `epodoc`: `US19780948554`\n- `docdb`: `US 19780948554`\n\n### Quota Monitoring\n\n```go\n// Get last quota information\nquota := client.GetLastQuota()\nif quota != nil {\n    fmt.Printf(\"Status: %s\\n\", quota.Status)\n    fmt.Printf(\"Usage: %.2f%%\\n\", quota.Individual.UsagePercent())\n}\n```\n\n## Configuration Options\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `ConsumerKey` | string | *required* | OAuth2 consumer key |\n| `ConsumerSecret` | string | *required* | OAuth2 consumer secret |\n| `BaseURL` | string | `https://ops.epo.org/3.2/rest-services` | API base URL |\n| `MaxRetries` | int | `3` | Maximum retry attempts |\n| `RetryDelay` | time.Duration | `1s` | Base delay between retries |\n| `Timeout` | time.Duration | `30s` | HTTP client timeout (increase for bulk classification endpoints) |\n\n## Error Handling\n\nThe library provides custom error types for different failure scenarios:\n\n```go\nbiblio, err := client.GetBiblio(ctx, \"publication\", \"docdb\", \"EP1000000B1\")\nif err != nil {\n    switch e := err.(type) {\n    case *ops.AuthError:\n        // Authentication failed\n        log.Printf(\"Auth error: %v\", e)\n    case *ops.NotFoundError:\n        // Patent not found (404)\n        log.Printf(\"Patent not found: %v\", e)\n    case *ops.QuotaExceededError:\n        // Fair use limit exceeded\n        log.Printf(\"Quota exceeded: %v\", e)\n    case *ops.ServiceUnavailableError:\n        // Temporary service outage\n        log.Printf(\"Service unavailable: %v\", e)\n    default:\n        // Other errors\n        log.Printf(\"Error: %v\", err)\n    }\n}\n```\n\n**Available Error Types**:\n- `AuthError` - Authentication failures\n- `NotFoundError` - Resource not found (404)\n- `QuotaExceededError` - Fair use quota exceeded (429, 403)\n- `ServiceUnavailableError` - Temporary service outage (503)\n- `AmbiguousPatentError` - Multiple kind codes available\n- `ConfigError` - Configuration issues\n\n## Retry Logic\n\nThe client automatically retries failed requests with exponential backoff:\n\n- **Retryable**: 5xx errors, 408, timeouts, network errors\n- **Non-retryable**: 404, 400, authentication errors, quota exceeded\n- **Token refresh**: Automatic on 401 errors\n- **Backoff**: Exponential with base delay × (attempt + 1)\n\nExample with custom retry configuration:\n\n```go\nconfig := \u0026ops.Config{\n    ConsumerKey:    \"your-key\",\n    ConsumerSecret: \"your-secret\",\n    MaxRetries:     5,                   // Try up to 5 times\n    RetryDelay:     2 * time.Second,     // Start with 2s delay\n}\nclient, err := ops.NewClient(config)\n```\n\n## Testing\n\nRun unit tests:\n```bash\ngo test -v\n```\n\nRun integration tests (requires credentials):\n```bash\nexport EPO_OPS_CONSUMER_KEY=\"your-key\"\nexport EPO_OPS_CONSUMER_SECRET=\"your-secret\"\ngo test -tags=integration -v\n```\n\n## Demo Application\n\nSee the [demo/](demo/) directory for a complete example application demonstrating all features.\n\n## API Specification\n\nThis library uses an OpenAPI 3.0 specification that was:\n- Converted from the official EPO OPS Swagger 2.0 specification\n- Extended to include the Data Usage Statistics endpoint (missing from official spec)\n- Enhanced with proper type definitions for generated code\n\nThe specification is maintained in [openapi.yaml](openapi.yaml) and used to generate strongly-typed client code.\n\n## Similar Projects\n\n- [epo-bdds](https://github.com/patent-dev/epo-bdds) - EPO Bulk Data Download Service client\n- [uspto-odp](https://github.com/patent-dev/uspto-odp) - USPTO Open Data Portal client\n\n## License\n\nMIT License - see [LICENSE](LICENSE) file for details.\n\n## Credits\n\n**Developed by:**\n- Wolfgang Stark - [patent.dev](https://patent.dev) - [Funktionslust GmbH](https://funktionslust.digital)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatent-dev%2Fepo-ops","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatent-dev%2Fepo-ops","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatent-dev%2Fepo-ops/lists"}