2D Game Engine using pyglet (OpenGL) for rendering
Project description
PyVerse2D
A batteries-included 2D game engine for Python, built on top of pyglet.
PyVerse2D gives you a complete ECS-based runtime - physics, lighting, particles, tilemaps, GUI, audio, video - behind a clean and minimal API.
Features
- ECS architecture - entities, components, and systems with dependency/conflict validation
- Physics engine - rigid bodies, gravity, collision detection and resolution (circles, capsules, polygons, ellipses, rounded rects)
- Rendering pipeline - layered scenes, cameras with letterboxing, viewport transforms, z-ordering
- Lighting system - ambient, point lights, cone lights, bloom, vignette, tint
- Particle system - line, circle, cone and point emitters with modifiers (wind, drag, gravity, attractor)
- Tilemap support - Tiled TMX loader, automatic collision injection, parallax cameras
- GUI system - widgets, tweens, behaviors (click, hover, focus, select), toggle buttons, scrollbars, labels
- Asset management - images, animations, fonts, sounds, music, playlists, video
- Input system - keyboard, mouse, combo listeners, repeat and condition support
- Post-processing - shader-based effects (blur, chromatic aberration, color grading, glitch, distortion, scanlines…) applied per zone with spatial masking
- Built-in profiler - frame-accurate profiling with export
Installation
pip install pyverse2d
Or install the latest dev version directly from GitHub:
pip install https://github.com/WhiteWolf45380/PyVerse2D/archive/refs/heads/main.zip
Requirements: Python 3.11+, pyglet 2.x
Hello World
The minimal setup: open a window and draw a bouncing ball.
import pyverse2d as pv
from pyverse2d import Window, LogicalScreen, Camera, Viewport
from pyverse2d import world, scene
# --- Window ---
screen = LogicalScreen(1920, 1080)
window = Window(screen=screen, caption="Hello World", vsync=True)
pv.set_window(window)
# --- Scene ---
camera = Camera(anchor=(0.5, 0.5), view_width=40, view_height=22.5)
viewport = Viewport(width=1920, height=1080, origin=(0.5, 0.5))
main_scene = scene.Scene(camera=camera, viewport=viewport)
scene.push(main_scene)
main_world = world.World()
main_scene.add_layer(scene.WorldLayer(main_world), z=0)
# --- Systems ---
main_world.add_system(world.RenderSystem())
main_world.add_system(world.PhysicsSystem())
main_world.add_system(world.GravitySystem(pv.math.Vector(0.0, -9.8)))
main_world.add_system(world.CollisionSystem())
# --- Ground ---
ground_shape = pv.shape.Rect(30.0, 1.0)
ground = world.Entity(
world.Transform(position=(0.0, -8.0), anchor=(0.5, 0.5)),
world.ShapeRenderer(shape=ground_shape, filling_color=(80, 80, 80)),
world.Collider(shape=ground_shape),
world.RigidBody(restitution=0.5, friction=0.4),
)
main_world.add_entity(ground)
# --- Ball ---
ball_shape = pv.shape.Circle(1.0)
ball = world.Entity(
world.Transform(position=(0.0, 8.0), anchor=(0.5, 0.5)),
world.ShapeRenderer(shape=ball_shape, filling_color=(80, 180, 255)),
world.Collider(shape=ball_shape),
world.RigidBody(mass=1.0, restitution=0.8, friction=0.2),
)
main_world.add_entity(ball)
# --- Run ---
pv.preload()
pv.run()
Internal objects
Assets
Assets are immutable descriptors - they hold configuration but no runtime state. They are cheap to create and safe to share.
# Image - width or height scales proportionally, scale_factor is a multiplier
img = pv.asset.Image("player.png", height=2.5)
img = pv.asset.Image("tile.png", scale_factor=2.0)
# Animation - built from an Image sequence
anim = pv.asset.Animation.from_folder("assets/", prefix="run_", framerate=12, height=2.5)
anim.duration # total duration in seconds
anim.frames # tuple of Images
# Font - system name, file path, or default
font = pv.asset.Font("Arial", size=24)
font = pv.asset.Font("fonts/custom.ttf", size=18)
font = pv.asset.Font() # built-in FreeSans fallback
font.text_width("Hello") # pixel width of a string
font.clip_text("Long string", max_width=200, suffix="…")
# Text - immutable content + font pair
text = pv.asset.Text("Score: 0", font=pv.asset.Font(size=20))
text.with_text("Score: 100") # returns a new Text
text.with_font(pv.asset.Font(size=32))
# Color - accepts int [0, 255] or float [0.0, 1.0] per channel, with optional alpha
red = pv.asset.Color(255, 0, 0)
white = pv.asset.Color(1.0, 1.0, 1.0)
semi = pv.asset.Color(0.2, 0.6, 1.0, 0.8)
semi.rgb # (r, g, b) as floats
semi.rgba8 # (r, g, b, a) as ints [0, 255]
semi.with_alpha(0.5)
# Sound - SFX, loaded entirely in memory
sound = pv.asset.Sound("hit.wav", volume=0.8, cooldown=0.1)
sound.play()
sound.play(loop=True, limit=3)
sound.pause()
sound.stop()
# Sound with random variations (picks one file at random each play)
footstep = pv.asset.Sound.from_variations("assets/audio/", prefix="step_", cooldown=0.4)
footstep.add_variation("assets/audio/step_extra.wav")
# Music - streamed from disk
music = pv.asset.Music("theme.ogg", volume=0.9)
music.play(loop=True, fade_s=2.0)
music.stop(fade_s=1.0)
# Video
video = pv.asset.Video("intro.mp4", volume=0.8)
# Playlist
playlist = pv.asset.Playlist(
[music_a, music_b, music_c],
shuffle=True, loop=True,
cross_fade=2.0, fade_in=1.0,
)
playlist.play()
playlist.add(music_d)
Bundles
A Bundle loads a whole folder of assets at once, keyed by filename (minus extension and optional prefix). Results are cached - calling get() twice for the same key returns the same instance.
# ImageBundle
images = pv.asset.ImageBundle.from_folder("assets/ui/", height=64)
btn_img = images.get("button") # Image
images.random() # random Image from the bundle
# AnimationBundle - not a built-in type, use Animation.from_folder directly
anim = pv.asset.Animation.from_folder("assets/run/", framerate=12, height=2.5)
# FontBundle
fonts = pv.asset.FontBundle.from_folder("assets/fonts/", size=24)
title_font = fonts.get("bold", size=48) # override size per call
# SoundBundle
sounds = pv.audio.load_sounds("assets/audio/sfx/", extensions=[".wav"], volume=1.0, cooldown=0.3)
sounds["footstep"].play()
sounds.random().play()
# MusicBundle - preloads each music in a background thread
musics = pv.audio.load_musics("assets/audio/music/", extensions=[".ogg"], volume=0.8)
musics.preload()
pv.audio.play_music(musics["theme"])
pv.audio.switch_music(musics.random(), fade_s=2.0)
# Common Bundle API
bundle.keys() # all keys
bundle.values_list() # all loaded assets
bundle.has("key")
len(bundle)
"key" in bundle
Rendering Pipeline
Window & screen
LogicalScreen defines the virtual resolution your game is designed for. Window wraps the OS window and handles letterboxing automatically, your game scales cleanly to any physical window size.
screen = LogicalScreen(1920, 1080) # virtual canvas
window = Window(screen=screen, resizable=True, vsync=True)
pv.set_window(window)
Viewport
A Viewport defines which region of the LogicalScreen a scene renders into, and how its local axes are oriented. By default a viewport covers the full screen with a standard basis, but you can split the screen, mirror axes, or skew the coordinate space.
# Full screen, centered origin (default setup)
viewport = Viewport(width=1920, height=1080, origin=(0.5, 0.5))
# Bottom-left quarter of the screen
viewport = Viewport(position=(0.0, 0.0), width=960, height=540, origin=(0.0, 0.0))
# Mirrored horizontal axis (flip x)
viewport = Viewport(
width=1920, height=1080,
origin=(0.5, 0.5),
x_direction=(-1.0, 0.0),
y_direction=(0.0, 1.0),
)
x_direction and y_direction form the local basis of the viewport - they must not be collinear. Changing them lets you rotate or mirror the rendered output independently of the camera.
Camera
Cameras define the point of view. They support smooth following, animated transitions, and parallax attachment.
# Follow a target with smooth lag
camera.follow(player.transform, offset=(0.0, 1.0), smoothing=0.05)
# Animated transition to a position
camera.goto((10.0, 0.0), duration=1.5, easing=pv.math.easing.ease_in_out_quad)
# Parallax-derived camera (e.g. for a background layer)
background_camera = Camera.derived_from(camera, parallax_x=0.3)
Core concepts
Scene & layers
A Scene is a self-contained game state. Layers stack inside a scene at a given z-index. WorldLayer, TileLayer, GuiLayer, LightLayer and ParticleLayer each handle a specific rendering domain.
main_scene = scene.Scene(camera=camera, viewport=viewport)
scene.push(main_scene) # push onto the scene stack
main_scene.add_layer(scene.WorldLayer(main_world), z=0) # ECS world
main_scene.add_layer(scene.GuiLayer(), z=100) # UI on top
Entities & components
An Entity is a container of components. Components are plain data objects, logic lives in systems.
player = world.Entity(
world.Transform(position=(0.0, 5.0), anchor=(0.5, 0.0)),
world.ShapeRenderer(shape=pv.shape.Capsule(0.4, 2.0), filling_color=(255, 120, 60)),
world.Collider(shape=pv.shape.Capsule(0.4, 2.0)),
world.RigidBody(mass=60.0, friction=0.4),
world.GroundSensor(),
)
main_world.add_entity(player)
Available components: Transform, ShapeRenderer, SpriteRenderer, TextRenderer, Animator, Collider, RigidBody, GroundSensor, Follow, SoundEmitter, VideoPlayer.
Systems
Systems process entities every frame. Add them to a World and they run in order:
main_world.add_system(world.RenderSystem())
main_world.add_system(world.PhysicsSystem())
main_world.add_system(world.GravitySystem(pv.math.Vector(0.0, -9.8)))
main_world.add_system(world.CollisionSystem())
main_world.add_system(world.AnimationSystem())
main_world.add_system(world.SteeringSystem())
main_world.add_system(world.SoundSystem(origin=camera))
Tilemaps
# Load a Tiled .tmx file
stage = pv.tile.MapLoader.from_tiled_tmx("map/stage_0.tmx", tile_width=1.5, tile_height=1.5)
# Add layers to the scene
ground = stage["ground"]
main_scene.add_layer(pv.scene.TileLayer(ground), z=4)
# Inject tile colliders automatically into the world
pv.tile.CollisionMapper(ground).inject(main_world)
Lighting
light_layer = pv.scene.LightLayer(
pv.fx.Ambient(intensity=0.2, color=(0, 0, 30)),
gamma=1.0,
exposure=3.0,
)
main_scene.add_layer(light_layer, z=3)
# Point light attached to the player
point = pv.fx.PointLight(radius=10, intensity=1.0)
point.attach_to(player.transform, offset=(0.0, 1.0))
light_layer.add_source(point)
# Cone light (e.g. spotlight from above)
cone = pv.fx.ConeLight(position=(0.0, 30.0), direction=(0.0, -1.0), angle=20, intensity=1.2)
light_layer.add_source(cone)
Particles
particle_layer = pv.scene.ParticleLayer(additive=True)
main_scene.add_layer(particle_layer, z=-1)
particle = pv.fx.Particle(
lifetime=(4, 8), speed=(5, 10),
size=(0.3, 0.8), size_end=0.1,
color_start=(0, 100, 255), color_end=(255, 255, 255),
)
emitter = pv.fx.LineEmitter(p1=(-50, 30), p2=(50, 30), normal=True, particle=particle, rate=80)
emitter.add_modifier(pv.fx.Wind(direction=-45, strength=1.5, turbulence=8))
particle_layer.add_emitter(emitter)
Post-processing
Post-processing effects are applied via a PostFxLayer. Each layer holds one or more PostFxZone objects, a zone can cover the entire screen (unbounded) or be restricted to a Circle or Rect shape with an optional soft blend edge. Effects chain in order and are GPU-accelerated (GLSL shaders).
postfx_layer = pv.scene.PostFxLayer()
main_scene.add_layer(postfx_layer, z=50)
# Unbounded zone: covers the full screen
zone = pv.fx.PostFxZone()
zone.add_effect(pv.fx.ColorGrade(brightness=0.05, contrast=1.1, saturation=0.9))
zone.add_effect(pv.fx.Vignette(strength=0.6, radius=80, softness=40))
postfx_layer.add_zone(zone)
# Bounded zone: circle around a point, with edge blending
local_zone = pv.fx.PostFxZone(shape=pv.shape.Circle(10), position=(0.0, 0.0), blend=3.0)
local_zone.add_effect(pv.fx.Blur(radius=4.0, passes=2))
postfx_layer.add_zone(local_zone)
# Attach a zone to a moving object
local_zone.attach_to(player.transform, offset=(0.0, 1.0), smoothing=0.05)
Available effects and their key parameters:
| Effect | Description | Key params |
|---|---|---|
Blur |
Gaussian blur (separable, multi-pass) | radius, passes |
Chromatic |
Chromatic aberration | strength, angle |
ColorGrade |
Brightness / contrast / saturation / tint | brightness, contrast, saturation, tint |
EdgeDetect |
Sobel edge detection | threshold, strength, edge_color |
Flicker |
Luminosity flicker (unstable light source) | amplitude, speed |
Glitch |
Digital corruption with band shifting | strength, density, speed |
Pixelate |
Pixelation | block_size |
Posterize |
Color level reduction (cel-shading) | levels |
Scanlines |
CRT scanline overlay | spacing, strength, softness |
Vignette |
Edge darkening | strength, radius, softness, color |
Wave |
Sinusoidal distortion (cartesian) | amplitude_x, amplitude_y, frequency_x, frequency_y, speed |
DistortSwirl |
Vortex rotation | angle, falloff |
DistortSqueeze |
Asymmetric directional stretch | strength_x, strength_y, falloff |
DistortRipple |
Radial concentric wave | amplitude, frequency, speed, falloff |
Effects can be added, removed, replaced and reordered at runtime:
zone.replace_effect(pv.fx.ColorGrade(saturation=0.0)) # swap params live
zone.move_effect(pv.fx.Blur, index=0) # push blur to first pass
zone.remove_effect(pv.fx.Vignette)
zone.disable() # pause the zone without removing it
Managers
KeyManager
pv.key tracks keyboard state. Key constants are exposed directly on the manager (K_A, K_SPACE, K_F11…).
# State queries (use these inside on_update)
pv.key.is_pressed(pv.key.K_D) # held down
pv.key.just_pressed(pv.key.K_SPACE) # pressed this frame
pv.key.just_released(pv.key.K_LSHIFT)
# Human-readable label
pv.key.name(pv.key.K_ENTER) # "Enter"
MouseManager
pv.mouse tracks cursor position, movement, scroll, and button state. Position is in LogicalScreen coordinates.
pv.mouse.position # (x, y) in logical space
pv.mouse.scroll_y # wheel delta this frame
pv.mouse.is_pressed(pv.mouse.B_LEFT)
pv.mouse.just_pressed(pv.mouse.B_RIGHT)
# World-space position under the cursor
wx, wy = pv.mouse.get_world_position(viewport=viewport, camera=camera)
# Cursor appearance
pv.mouse.set_appearance(pv.mouse.SystemMouseCursor(pv.mouse.CURSOR_HAND))
pv.mouse.set_appearance(pv.mouse.ImageMouseCursor(img, anchor_x=0.0, anchor_y=1.0))
# Exclusive mode (FPS-style grab)
pv.mouse.set_exclusive(True)
InputsManager
pv.inputs maps keys and mouse buttons to callbacks. All listeners return a Listener object that can be enabled, disabled, or invalidated at any time.
# Single key, fires on press
pv.inputs.add_listener(pv.key.K_SPACE, player.jump)
# Held key - fires every frame while pressed
pv.inputs.add_listener(pv.key.K_D, player.move_right, repeat=True)
# Release
pv.inputs.add_listener(pv.key.K_LSHIFT, on_sprint_end, up=True)
# Conditional - only fires when the lambda returns True
pv.inputs.add_listener(pv.key.K_RIGHT, cam_right, repeat=True, condition=lambda: not pv.key.is_pressed(pv.key.K_LEFT))
# Any of a set of keys
pv.inputs.when_any_of([pv.key.K_A, pv.key.K_LEFT], player.move_left, repeat=True)
# Combo - all keys held simultaneously
pv.inputs.when_all_of([pv.key.K_LEFT, pv.key.K_DOWN], move_downleft, repeat=True)
# Mouse button
pv.inputs.add_listener(pv.mouse.B_LEFT, on_click)
# Fire once then auto-remove
pv.inputs.add_listener(pv.key.K_ENTER, on_confirm, once=True)
# Manage a listener
listener = pv.inputs.add_listener(pv.key.K_P, on_pause)
listener.disable() # temporarily mute
listener.enable()
listener.invalidate() # remove permanently
EventManager
pv.event gives direct access to raw pyglet events via subscribable slots. Each slot supports a priority and a consume flag to stop propagation.
# Subscribe
pv.event.on_key_press.subscribe(my_handler, priority=10)
pv.event.on_mouse_scroll.subscribe(on_scroll)
pv.event.on_resize.subscribe(on_resize)
# Unsubscribe
pv.event.on_key_press.unsubscribe(my_handler)
# Stop propagation from inside a handler
def my_handler(symbol):
if symbol == pv.key.K_ESCAPE:
return pv.event.ConsumeFlag
Available slots: on_key_press, on_key_release, on_text, on_text_motion, on_text_motion_select, on_mouse_press, on_mouse_release, on_mouse_motion, on_mouse_drag, on_mouse_scroll, on_mouse_enter, on_mouse_leave, on_resize, on_close, on_activate, on_deactivate, on_show, on_hide, on_move.
TimeManager
pv.time manages the game clock, delta-time, FPS cap, time scaling, and deferred callbacks.
pv.time.clock # total elapsed time (seconds)
pv.time.frame # total frame count
pv.time.dt # capped delta-time
pv.time.fps # current FPS
pv.time.smooth_fps # averaged over last 50 frames
pv.time.target_fps = 120 # cap framerate
pv.time.time_scale = 0.5 # slow motion
# Deferred call
pv.time.after(2.0, spawn_enemy) # once after 2 s
pv.time.every(10.0, lambda: switch_music) # repeat every 10 s
AudioManager
pv.audio handles sound effects and music, including crossfades, playlists, and sound groups.
# Sound effects
sound = pv.asset.Sound("hit.wav", volume=0.8, cooldown=0.1)
pv.audio.play_sound(sound)
pv.audio.play_sound(sound, loop=True, limit=3)
pv.audio.pause_sound(sound)
pv.audio.stop_sound(sound)
# Volume groups
sfx_group = pv.audio.SoundGroup(volume=0.6, pool_max=8)
sound = pv.asset.Sound("step.wav", group=sfx_group)
sfx_group.pause_all()
# Music
music = pv.asset.Music("theme.ogg")
pv.audio.play_music(music, loop=True, fade_s=2.0)
pv.audio.switch_music(other_music, fade_s=1.5) # crossfade
pv.audio.pause_music()
pv.audio.stop_music(fade_s=1.0)
# Playlist
playlist = pv.asset.Playlist(musics.values_list(), shuffle=True, loop=True, cross_fade=2.0)
pv.audio.play_playlist(playlist)
# Master volumes
pv.audio.master_volume = 0.8
pv.audio.music_volume = 0.5
CoordinatesManager
pv.coordinates converts positions between the four coordinate spaces: World, Framebuffer (logical pixels), and Window (physical pixels).
# World → screen
fx, fy = pv.coordinates.world_to_framebuffer(entity.x, entity.y)
# Window (OS pixels) → world
wx, wy = pv.coordinates.window_to_world(raw_x, raw_y)
# Frustum corners and AABB (useful for culling)
corners = pv.coordinates.get_frustum_corners()
x_min, y_min, x_max, y_max = pv.coordinates.get_frustum_aabb()
# Generic converter
x, y = pv.coordinates.convert(x, y, from_space="world", to_space="framebuffer")
A context must be applied before converting (done automatically each frame inside layers). Use temporary_context to convert with a specific camera or viewport:
with pv.coordinates.temporary_context(viewport=my_viewport, camera=my_camera):
wx, wy = pv.coordinates.framebuffer_to_world(fx, fy)
UIManager
pv.ui arbitrates widget focus and hover state. It is mostly used internally by the GUI system but can be queried directly.
pv.ui.hovered # currently hovered widget, or None
pv.ui.focused # currently focused widget, or None
pv.ui.unhover()
pv.ui.unfocus()
Run Configuration
Profiling
pv.preload()
pv.profile(duration=10.0, on_update=on_update, export_path="profile_report.txt")
Game loop
def on_update(dt: float):
# your update logic
pass
def on_draw():
# your extra draw logic (runs before scene.draw)
pass
pv.preload()
pv.run(on_update=on_update, on_draw=on_draw)
pv.stop() cleanly shuts down the engine and closes the window.
Project structure
pyverse2d/
├── abc/ # Abstract base classes (Space, Component, System, Asset…)
├── asset/ # Image, Animation, Font, Sound, Music, Video, Text, Playlist
├── fx/
│ ├── light/ # Ambient, PointLight, ConeLight, Bloom, Vignette, Tint
│ ├── particle/ # Emitters (Line, Circle, Cone, Point) + Modifiers
│ └── postfx/ # PostFxZone, PostFxLayer + 14 shader effects
├── gui/ # Widgets, Tweens, Behaviors, SelectionGroup
├── math/ # Point, Vector, Line, easing functions, vertex helpers
├── scene/ # Scene, WorldLayer, TileLayer, GuiLayer, LightLayer, ParticleLayer, PostFxLayer
├── shape/ # Circle, Rect, RoundedRect, Ellipse, Capsule, Polygon, RegularPolygon
├── tile/ # Tiled TMX loader, CollisionMapper, TileMap
├── typing/ # Type aliases
└── world/
├── _component/ # Transform, Collider, RigidBody, Renderers, Animator, Follow…
└── _system/ # Physics, Gravity, Collision, Render, Animation, Steering, Sound, Video
License
MIT - © WhiteWolf45380
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyverse2d-1.5.13.tar.gz.
File metadata
- Download URL: pyverse2d-1.5.13.tar.gz
- Upload date:
- Size: 279.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0df2992d5bba3cfc3281f7c3f0793865bfac7f8013c3b94fd7e3a608f8413e35
|
|
| MD5 |
c0b8cc0f963fd53cec0fc64366fcf409
|
|
| BLAKE2b-256 |
29e983203212ae8d4dcb6e1de8bc8a9187b182ee66511e2119037703770e6b75
|
Provenance
The following attestation bundles were made for pyverse2d-1.5.13.tar.gz:
Publisher:
publish.yml on WhiteWolf45380/PyVerse2D
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyverse2d-1.5.13.tar.gz -
Subject digest:
0df2992d5bba3cfc3281f7c3f0793865bfac7f8013c3b94fd7e3a608f8413e35 - Sigstore transparency entry: 1594909462
- Sigstore integration time:
-
Permalink:
WhiteWolf45380/PyVerse2D@d7d0e877150324000f337ddfb9c7788901697817 -
Branch / Tag:
refs/tags/v1.5.13 - Owner: https://github.com/WhiteWolf45380
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d7d0e877150324000f337ddfb9c7788901697817 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyverse2d-1.5.13-py3-none-any.whl.
File metadata
- Download URL: pyverse2d-1.5.13-py3-none-any.whl
- Upload date:
- Size: 391.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43342c811bfbb3f127817e60aebc4a9f2b00cc29acc479a5a3c6659e8b51994a
|
|
| MD5 |
a0d87d6829ee2038c8b782d2e4ac6ee9
|
|
| BLAKE2b-256 |
b7b3b6a061835dec04198fdeca2de37d1b2df6417bacbb713206e7fc2c4df081
|
Provenance
The following attestation bundles were made for pyverse2d-1.5.13-py3-none-any.whl:
Publisher:
publish.yml on WhiteWolf45380/PyVerse2D
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyverse2d-1.5.13-py3-none-any.whl -
Subject digest:
43342c811bfbb3f127817e60aebc4a9f2b00cc29acc479a5a3c6659e8b51994a - Sigstore transparency entry: 1594909505
- Sigstore integration time:
-
Permalink:
WhiteWolf45380/PyVerse2D@d7d0e877150324000f337ddfb9c7788901697817 -
Branch / Tag:
refs/tags/v1.5.13 - Owner: https://github.com/WhiteWolf45380
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d7d0e877150324000f337ddfb9c7788901697817 -
Trigger Event:
push
-
Statement type: