# fpm-py
A Python library for Fourier Ptychographic Microscopy (FPM) simulation and reconstruction with GPU acceleration.
## Overview
This library implements both the **forward pass** (reconstruction from captures) and **backward pass** (simulation of captures from an object) for Fourier ptychography. It uses PyTorch for efficient GPU-accelerated computation and provides a clean, type-safe API for research and development.
Fourier ptychography synthesizes a high-resolution, wide-field image by computationally combining multiple low-resolution images captured under varying illumination angles. This creates a synthetic aperture larger than the physical objective NA, enabling resolution enhancement while maintaining a large field of view.
## Installation
```bash
pip install fpm-py
```
Or with `uv` (preferred):
```bash
uv add fpm-py
```
**Requirements**: 3.12 ≥ Python ≥ 3.9, PyTorch ≥ 2.8.0
## Quick Start
```python
import fpm_py as fpm
from fpm_py.utils.data_utils import image_to_tensor
import torch
# Load a test object
obj = image_to_tensor("path/to/image.png", to_complex=True)
# Define k-vectors (illumination angles) - 5×5 grid
grid_size = 5
spacing = 0.2
k_range = torch.linspace(-(grid_size // 2), grid_size // 2, grid_size) * spacing
kx, ky = torch.meshgrid(k_range, k_range, indexing="ij")
k_vectors = torch.stack((kx.flatten(), ky.flatten()), dim=1)
# Simulate FPM captures (backward pass)
dataset = fpm.kvectors_to_image_series(
obj=obj,
k_vectors=k_vectors,
pupil_radius=100,
wavelength=550e-9, # 550 nm (green light)
pixel_size=1e-6, # 1 μm pixels
magnification=10.0 # 10× objective
)
# Reconstruct high-resolution image (forward pass)
reconstruction = fpm.reconstruct(dataset, output_scale_factor=4, max_iters=10)
# View magnitude
import matplotlib.pyplot as plt
plt.imshow(reconstruction.abs().cpu().numpy(), cmap='gray')
plt.show()
```
For a more detailed walkthrough, see [main.py](https://github.com/rspcunningham/fpm-py/blob/main/main.py).
## Core Concepts
### Data Structures
#### `ImageCapture`
Represents a single FPM capture with its associated k-vector:
```python
@dataclass(frozen=True)
class ImageCapture:
image: torch.Tensor # (H, W) intensity image
k_vector: torch.Tensor # (2,) illumination angle in k-space
```
#### `ImageSeries`
Container for multiple captures with acquisition settings:
```python
@dataclass
class ImageSeries:
captures: Sequence[ImageCapture]
settings: AcquisitionSettings # du, wavelength, pixel_size
device: torch.device
```
The `du` parameter is the **k-space lattice spacing** — it relates pixel coordinates in Fourier space to physical spatial frequencies. Automatically computed as:
```python
du = wavelength / (N × effective_pixel_size)
```
where `effective_pixel_size = pixel_size / magnification`.
### Backward Pass: Simulation
Generate synthetic FPM captures from a known object (inverse of reconstruction):
```python
series = fpm.kvectors_to_image_series(
obj=complex_field, # Complex object (amplitude + phase)
k_vectors=k_vectors, # (N, 2) illumination directions
pupil_radius=100, # Pupil radius in pixels (related to NA)
wavelength=550e-9, # Illumination wavelength [m]
pixel_size=1e-6, # Camera pixel size [m]
magnification=10.0, # Optical magnification
pupil=None # Optional custom pupil function
)
```
**Physics**: For each k-vector `(kx, ky)`, the backward pass:
1. Applies a tilted plane wave: `obj × exp(i(kx·x + ky·y))`
2. Computes Fourier transform and applies pupil mask (optical transfer function)
3. Inverse transforms back to spatial domain
4. Records intensity: `I = |wave|²`
This simulates the effect of different illuminations in a physical FPM system, given a 'gold standard' image (ie. target reconstruction).
### Forward Pass: Reconstruction
Recover the high-resolution complex field from captures (the primary FPM algorithm):
```python
reconstructed = fpm.reconstruct(
series=image_series,
output_scale_factor=4, # Output is 4× larger than input captures
max_iters=10, # Number of iterative refinement passes
pupil_0=None # Optional initial pupil estimate
)
```
**Returns**: Complex-valued tensor in spatial domain with shape `(H_out, W_out)` where the output size is either:
- `output_scale_factor × input_size` (if specified)
- Auto-calculated minimum size for full k-space coverage
- Explicitly set via `output_image_size=(H, W)`
#### Reconstruction Algorithm
The implementation uses a **quasi-Newton phase retrieval** approach based on Tian et al. (2014):
1. **Initialization**: First capture initializes the high-res spectrum
2. **Iterative Update Loop**: For each capture at k-vector `(kx, ky)`:
- Extract relevant Fourier patch from global object estimate
- Apply pupil function and transform to spatial domain
- **Intensity constraint**: Replace estimated magnitude with √(measured intensity)
- **Back-propagate**: Transform corrected wave to Fourier domain
- **Joint optimization**: Update both object spectrum and pupil using quasi-Newton step
- **Support constraint**: Enforce binary pupil support
The quasi-Newton update (Tian's method) balances updates between object and pupil:
```python
# Object update
obj_update = pupil* · Δwave / (|pupil|² + α)
# Pupil update
pupil_update = obj* · Δwave / (|obj|² + β)
```
where `Δwave = wave_corrected - wave_estimated` and α, β are regularization parameters.
**Key parameters**:
- `alpha=1.0`: Object update regularization (prevents division by zero)
- `beta=1000.0`: Pupil update regularization (pupil converges slower than object)
### Converting Physical LED Positions
If you have physical LED array coordinates, convert them to k-vectors:
```python
import numpy as np
# LED positions in 3D space (meters), with (0,0,0) at optical axis
led_positions = np.array([
[0.01, 0.0, 0.05], # x, y, z for each LED
[0.0, 0.01, 0.05],
# ... more LEDs
])
k_vectors = fpm.spatial_to_kvectors(
led_positions=led_positions,
wavelength=550e-9,
pixel_size=1e-6,
sample_to_lens_distance=0.05, # 50 mm
lens_to_sensor_distance=0.50 # 500 mm (10× magnification)
)
# Now use k_vectors with kvectors_to_image_series()
```
## Evaluation & Visualization
Compare reconstruction quality:
```python
from fpm_py import plot_comparison_with_histograms
# Create comparison plots with histograms and Fourier spectra
fig, stats = plot_comparison_with_histograms(
images=[target, recon_1iter, recon_10iter],
titles=["Ground Truth", "1 Iteration", "10 Iterations"],
reference_idx=0 # Use first image as reference for stat calculations
)
plt.show()
# Access quantitative metrics
print(f"SSIM: {stats['ssim']}")
print(f"PSNR: {stats['psnr']}")
```
The `stats` dictionary includes:
- **SSIM**: Structural similarity index
- **PSNR**: Peak signal-to-noise ratio
- **MSE/RMSE**: Mean squared error metrics
- **NCC**: Normalized cross-correlation
- Plus histogram statistics (mean, std, skewness, kurtosis, entropy)
## Implementation Details
### Device Management
The library automatically selects the best available device (CUDA > MPS > CPU):
```python
from fpm_py import best_device
device = best_device() # Auto-selects GPU if available
series = series.to(device) # Move data to device
```
### FFT Conventions
All Fourier transforms use **centered** FFTs via `fftshift`/`ifftshift`:
```python
def ft(x):
return torch.fft.fftshift(torch.fft.fft2(torch.fft.ifftshift(x)))
def ift(x):
return torch.fft.ifftshift(torch.fft.ifft2(torch.fft.fftshift(x)))
```
This ensures DC component is at the array center, matching the physics convention where k=0 is the optical axis.
### Memory Optimization
- All computations use `torch.no_grad()` (no autograd overhead)
- Float32 precision throughout (GPU-optimized)
- In-place operations where possible (`add_`, `mul_`, `div_`)
- Output dimensions forced to even numbers for FFT efficiency
### Coordinate System
**k-space**: The k-vector `(kx, ky)` represents the **transverse** component of the wavevector, scaled by effective pixel size:
```
k_scaled = (2π/λ) sin(θ) × (pixel_size / magnification)
```
where θ is the illumination angle from the optical axis.
**Fourier patch extraction**: For k-vector `(kx, ky)`, the crop location in the global spectrum is:
```python
x = center_x + int(kx / du)
y = center_y + int(ky / du)
```
This shift-and-multiply theorem maps illumination angles to Fourier space shifts.
## Architecture
```
src/fpm_py/
├── core/
│ ├── structs.py # Data structures (ImageCapture, ImageSeries)
│ ├── forward.py # Reconstruction algorithm (forward pass)
│ └── backward.py # Simulation (backward pass)
├── utils/
│ ├── math_utils.py # FFT helpers, geometry, overlap operations
│ └── data_utils.py # Image loading, device selection
└── analysis/
└── metrics.py # SSIM, PSNR, visualization
```
**Note**: The naming convention:
- `forward.py` = **reconstruction** (captures → high-res image) - the forward pass
- `backward.py` = **simulation** (high-res image → captures) - the backward pass (inverse of reconstruction)
## References
This implementation is based on:
1. **Tian et al. (2014)** - "Multiplexed coded illumination for Fourier Ptychography with an LED array microscope" - Quasi-Newton optimization
2. **Zheng et al. (2013)** - "Wide-field, high-resolution Fourier ptychographic microscopy" - Original FPM paper
3. **Ou et al. (2015)** - "Embedded pupil function recovery for Fourier ptychographic microscopy" - Pupil recovery method
For the mathematical foundations of Fourier ptychography, see: https://en.wikipedia.org/wiki/Fourier_ptychography
## Example: Full Pipeline
See [`main.py`](main.py) for a complete working example that:
1. Loads a test image (USAF resolution target)
2. Generates a 5×5 grid of k-vectors
3. Simulates FPM captures
4. Reconstructs with 1 and 10 iterations
5. Visualizes results with comparison metrics
## License
MIT License - See LICENSE file for details.
## Contributing
Issues and pull requests welcome at: https://github.com/rspcunningham/fpm-py
---
**Version**: 2.0.0 | **Author**: Robin Cunningham
Raw data
{
"_id": null,
"home_page": null,
"name": "fpm-py",
"maintainer": null,
"docs_url": null,
"requires_python": "<3.13,>=3.9",
"maintainer_email": null,
"keywords": "fourier, ptychography, microscopy, imaging, reconstruction",
"author": null,
"author_email": "Robin Cunningham <rspcunningham@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/66/b3/eb70eae208493cbb337e4eb68430a97d3e5fa342d4c3f90c4d5c07674443/fpm_py-2.0.1.tar.gz",
"platform": null,
"description": "# fpm-py\n\nA Python library for Fourier Ptychographic Microscopy (FPM) simulation and reconstruction with GPU acceleration.\n\n## Overview\n\nThis library implements both the **forward pass** (reconstruction from captures) and **backward pass** (simulation of captures from an object) for Fourier ptychography. It uses PyTorch for efficient GPU-accelerated computation and provides a clean, type-safe API for research and development.\n\nFourier ptychography synthesizes a high-resolution, wide-field image by computationally combining multiple low-resolution images captured under varying illumination angles. This creates a synthetic aperture larger than the physical objective NA, enabling resolution enhancement while maintaining a large field of view.\n\n## Installation\n\n```bash\npip install fpm-py\n```\n\nOr with `uv` (preferred):\n```bash\nuv add fpm-py\n```\n\n**Requirements**: 3.12 \u2265 Python \u2265 3.9, PyTorch \u2265 2.8.0\n\n## Quick Start\n\n```python\nimport fpm_py as fpm\nfrom fpm_py.utils.data_utils import image_to_tensor\nimport torch\n\n# Load a test object\nobj = image_to_tensor(\"path/to/image.png\", to_complex=True)\n\n# Define k-vectors (illumination angles) - 5\u00d75 grid\ngrid_size = 5\nspacing = 0.2\nk_range = torch.linspace(-(grid_size // 2), grid_size // 2, grid_size) * spacing\nkx, ky = torch.meshgrid(k_range, k_range, indexing=\"ij\")\nk_vectors = torch.stack((kx.flatten(), ky.flatten()), dim=1)\n\n# Simulate FPM captures (backward pass)\ndataset = fpm.kvectors_to_image_series(\n obj=obj,\n k_vectors=k_vectors,\n pupil_radius=100,\n wavelength=550e-9, # 550 nm (green light)\n pixel_size=1e-6, # 1 \u03bcm pixels\n magnification=10.0 # 10\u00d7 objective\n)\n\n# Reconstruct high-resolution image (forward pass)\nreconstruction = fpm.reconstruct(dataset, output_scale_factor=4, max_iters=10)\n\n# View magnitude\nimport matplotlib.pyplot as plt\nplt.imshow(reconstruction.abs().cpu().numpy(), cmap='gray')\nplt.show()\n```\n\nFor a more detailed walkthrough, see [main.py](https://github.com/rspcunningham/fpm-py/blob/main/main.py).\n\n## Core Concepts\n\n### Data Structures\n\n#### `ImageCapture`\nRepresents a single FPM capture with its associated k-vector:\n```python\n@dataclass(frozen=True)\nclass ImageCapture:\n image: torch.Tensor # (H, W) intensity image\n k_vector: torch.Tensor # (2,) illumination angle in k-space\n```\n\n#### `ImageSeries`\nContainer for multiple captures with acquisition settings:\n```python\n@dataclass\nclass ImageSeries:\n captures: Sequence[ImageCapture]\n settings: AcquisitionSettings # du, wavelength, pixel_size\n device: torch.device\n```\n\nThe `du` parameter is the **k-space lattice spacing** \u2014 it relates pixel coordinates in Fourier space to physical spatial frequencies. Automatically computed as:\n\n```python\ndu = wavelength / (N \u00d7 effective_pixel_size)\n```\n\nwhere `effective_pixel_size = pixel_size / magnification`.\n\n### Backward Pass: Simulation\n\nGenerate synthetic FPM captures from a known object (inverse of reconstruction):\n\n```python\nseries = fpm.kvectors_to_image_series(\n obj=complex_field, # Complex object (amplitude + phase)\n k_vectors=k_vectors, # (N, 2) illumination directions\n pupil_radius=100, # Pupil radius in pixels (related to NA)\n wavelength=550e-9, # Illumination wavelength [m]\n pixel_size=1e-6, # Camera pixel size [m]\n magnification=10.0, # Optical magnification\n pupil=None # Optional custom pupil function\n)\n```\n\n**Physics**: For each k-vector `(kx, ky)`, the backward pass:\n1. Applies a tilted plane wave: `obj \u00d7 exp(i(kx\u00b7x + ky\u00b7y))`\n2. Computes Fourier transform and applies pupil mask (optical transfer function)\n3. Inverse transforms back to spatial domain\n4. Records intensity: `I = |wave|\u00b2`\n\nThis simulates the effect of different illuminations in a physical FPM system, given a 'gold standard' image (ie. target reconstruction).\n\n### Forward Pass: Reconstruction\n\nRecover the high-resolution complex field from captures (the primary FPM algorithm):\n\n```python\nreconstructed = fpm.reconstruct(\n series=image_series,\n output_scale_factor=4, # Output is 4\u00d7 larger than input captures\n max_iters=10, # Number of iterative refinement passes\n pupil_0=None # Optional initial pupil estimate\n)\n```\n\n**Returns**: Complex-valued tensor in spatial domain with shape `(H_out, W_out)` where the output size is either:\n- `output_scale_factor \u00d7 input_size` (if specified)\n- Auto-calculated minimum size for full k-space coverage\n- Explicitly set via `output_image_size=(H, W)`\n\n#### Reconstruction Algorithm\n\nThe implementation uses a **quasi-Newton phase retrieval** approach based on Tian et al. (2014):\n\n1. **Initialization**: First capture initializes the high-res spectrum\n2. **Iterative Update Loop**: For each capture at k-vector `(kx, ky)`:\n - Extract relevant Fourier patch from global object estimate\n - Apply pupil function and transform to spatial domain\n - **Intensity constraint**: Replace estimated magnitude with \u221a(measured intensity)\n - **Back-propagate**: Transform corrected wave to Fourier domain\n - **Joint optimization**: Update both object spectrum and pupil using quasi-Newton step\n - **Support constraint**: Enforce binary pupil support\n\nThe quasi-Newton update (Tian's method) balances updates between object and pupil:\n\n```python\n# Object update\nobj_update = pupil* \u00b7 \u0394wave / (|pupil|\u00b2 + \u03b1)\n\n# Pupil update\npupil_update = obj* \u00b7 \u0394wave / (|obj|\u00b2 + \u03b2)\n```\n\nwhere `\u0394wave = wave_corrected - wave_estimated` and \u03b1, \u03b2 are regularization parameters.\n\n**Key parameters**:\n- `alpha=1.0`: Object update regularization (prevents division by zero)\n- `beta=1000.0`: Pupil update regularization (pupil converges slower than object)\n\n### Converting Physical LED Positions\n\nIf you have physical LED array coordinates, convert them to k-vectors:\n\n```python\nimport numpy as np\n\n# LED positions in 3D space (meters), with (0,0,0) at optical axis\nled_positions = np.array([\n [0.01, 0.0, 0.05], # x, y, z for each LED\n [0.0, 0.01, 0.05],\n # ... more LEDs\n])\n\nk_vectors = fpm.spatial_to_kvectors(\n led_positions=led_positions,\n wavelength=550e-9,\n pixel_size=1e-6,\n sample_to_lens_distance=0.05, # 50 mm\n lens_to_sensor_distance=0.50 # 500 mm (10\u00d7 magnification)\n)\n\n# Now use k_vectors with kvectors_to_image_series()\n```\n\n## Evaluation & Visualization\n\nCompare reconstruction quality:\n\n```python\nfrom fpm_py import plot_comparison_with_histograms\n\n# Create comparison plots with histograms and Fourier spectra\nfig, stats = plot_comparison_with_histograms(\n images=[target, recon_1iter, recon_10iter],\n titles=[\"Ground Truth\", \"1 Iteration\", \"10 Iterations\"],\n reference_idx=0 # Use first image as reference for stat calculations\n)\nplt.show()\n\n# Access quantitative metrics\nprint(f\"SSIM: {stats['ssim']}\")\nprint(f\"PSNR: {stats['psnr']}\")\n```\n\nThe `stats` dictionary includes:\n- **SSIM**: Structural similarity index\n- **PSNR**: Peak signal-to-noise ratio\n- **MSE/RMSE**: Mean squared error metrics\n- **NCC**: Normalized cross-correlation\n- Plus histogram statistics (mean, std, skewness, kurtosis, entropy)\n\n## Implementation Details\n\n### Device Management\n\nThe library automatically selects the best available device (CUDA > MPS > CPU):\n\n```python\nfrom fpm_py import best_device\n\ndevice = best_device() # Auto-selects GPU if available\nseries = series.to(device) # Move data to device\n```\n\n### FFT Conventions\n\nAll Fourier transforms use **centered** FFTs via `fftshift`/`ifftshift`:\n\n```python\ndef ft(x):\n return torch.fft.fftshift(torch.fft.fft2(torch.fft.ifftshift(x)))\n\ndef ift(x):\n return torch.fft.ifftshift(torch.fft.ifft2(torch.fft.fftshift(x)))\n```\n\nThis ensures DC component is at the array center, matching the physics convention where k=0 is the optical axis.\n\n### Memory Optimization\n\n- All computations use `torch.no_grad()` (no autograd overhead)\n- Float32 precision throughout (GPU-optimized)\n- In-place operations where possible (`add_`, `mul_`, `div_`)\n- Output dimensions forced to even numbers for FFT efficiency\n\n### Coordinate System\n\n**k-space**: The k-vector `(kx, ky)` represents the **transverse** component of the wavevector, scaled by effective pixel size:\n\n```\nk_scaled = (2\u03c0/\u03bb) sin(\u03b8) \u00d7 (pixel_size / magnification)\n```\n\nwhere \u03b8 is the illumination angle from the optical axis.\n\n**Fourier patch extraction**: For k-vector `(kx, ky)`, the crop location in the global spectrum is:\n\n```python\nx = center_x + int(kx / du)\ny = center_y + int(ky / du)\n```\n\nThis shift-and-multiply theorem maps illumination angles to Fourier space shifts.\n\n## Architecture\n\n```\nsrc/fpm_py/\n\u251c\u2500\u2500 core/\n\u2502 \u251c\u2500\u2500 structs.py # Data structures (ImageCapture, ImageSeries)\n\u2502 \u251c\u2500\u2500 forward.py # Reconstruction algorithm (forward pass)\n\u2502 \u2514\u2500\u2500 backward.py # Simulation (backward pass)\n\u251c\u2500\u2500 utils/\n\u2502 \u251c\u2500\u2500 math_utils.py # FFT helpers, geometry, overlap operations\n\u2502 \u2514\u2500\u2500 data_utils.py # Image loading, device selection\n\u2514\u2500\u2500 analysis/\n \u2514\u2500\u2500 metrics.py # SSIM, PSNR, visualization\n```\n\n**Note**: The naming convention:\n- `forward.py` = **reconstruction** (captures \u2192 high-res image) - the forward pass\n- `backward.py` = **simulation** (high-res image \u2192 captures) - the backward pass (inverse of reconstruction)\n\n## References\n\nThis implementation is based on:\n\n1. **Tian et al. (2014)** - \"Multiplexed coded illumination for Fourier Ptychography with an LED array microscope\" - Quasi-Newton optimization\n2. **Zheng et al. (2013)** - \"Wide-field, high-resolution Fourier ptychographic microscopy\" - Original FPM paper\n3. **Ou et al. (2015)** - \"Embedded pupil function recovery for Fourier ptychographic microscopy\" - Pupil recovery method\n\nFor the mathematical foundations of Fourier ptychography, see: https://en.wikipedia.org/wiki/Fourier_ptychography\n\n## Example: Full Pipeline\n\nSee [`main.py`](main.py) for a complete working example that:\n1. Loads a test image (USAF resolution target)\n2. Generates a 5\u00d75 grid of k-vectors\n3. Simulates FPM captures\n4. Reconstructs with 1 and 10 iterations\n5. Visualizes results with comparison metrics\n\n## License\n\nMIT License - See LICENSE file for details.\n\n## Contributing\n\nIssues and pull requests welcome at: https://github.com/rspcunningham/fpm-py\n\n---\n\n**Version**: 2.0.0 | **Author**: Robin Cunningham\n",
"bugtrack_url": null,
"license": null,
"summary": "Fourier ptychography tooling for simulation and reconstruction",
"version": "2.0.1",
"project_urls": {
"Homepage": "https://github.com/rspcunningham/fpm-py",
"Issues": "https://github.com/rspcunningham/fpm-py/issues",
"Repository": "https://github.com/rspcunningham/fpm-py"
},
"split_keywords": [
"fourier",
" ptychography",
" microscopy",
" imaging",
" reconstruction"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "09f19ef59f2427bae9b1d53c5318e3a6d0236f4d81d6e552a66d7d807094df58",
"md5": "514df436841f648c4927f4e16021fd80",
"sha256": "6a2708de8c48951b5e4d340046a1ca2237ba06264fa3e94a185d9562829e688d"
},
"downloads": -1,
"filename": "fpm_py-2.0.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "514df436841f648c4927f4e16021fd80",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<3.13,>=3.9",
"size": 21890,
"upload_time": "2025-10-08T04:38:40",
"upload_time_iso_8601": "2025-10-08T04:38:40.667025Z",
"url": "https://files.pythonhosted.org/packages/09/f1/9ef59f2427bae9b1d53c5318e3a6d0236f4d81d6e552a66d7d807094df58/fpm_py-2.0.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "66b3eb70eae208493cbb337e4eb68430a97d3e5fa342d4c3f90c4d5c07674443",
"md5": "180ea8910bead7a1782cb3e5db28ca08",
"sha256": "c8d55e4b45392fd5b9b9610bcae7fa167e8057a88d528f8919ba6c090a1a83a2"
},
"downloads": -1,
"filename": "fpm_py-2.0.1.tar.gz",
"has_sig": false,
"md5_digest": "180ea8910bead7a1782cb3e5db28ca08",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<3.13,>=3.9",
"size": 23674,
"upload_time": "2025-10-08T04:38:42",
"upload_time_iso_8601": "2025-10-08T04:38:42.079077Z",
"url": "https://files.pythonhosted.org/packages/66/b3/eb70eae208493cbb337e4eb68430a97d3e5fa342d4c3f90c4d5c07674443/fpm_py-2.0.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-10-08 04:38:42",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "rspcunningham",
"github_project": "fpm-py",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "fpm-py"
}