Skip to content

Commit

Permalink
add examples, support other languages (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin authored Jan 27, 2025
1 parent 84be232 commit 7f1a7f2
Show file tree
Hide file tree
Showing 16 changed files with 294 additions and 75 deletions.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2025 - present Pydantic Services inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# pydantic.run
# [pydantic.run](https://pydantic.dev)

Python browser sandbox.
Python browser sandbox. Write and share Python code, run it in the browser.

Built to demonstrate [Pydantic](https://docs.pydantic.dev), [PydanticAI](https://ai.pydantic.dev), and [Pydantic Logfire](https://docs.pydantic.dev/logfire).
2 changes: 1 addition & 1 deletion src/cf_worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default {
return await api(url, request, env)
} else if (url.pathname === '/new' || url.pathname === '/new/') {
return await createNew(url, request, env)
} else if (url.pathname.startsWith('/store/')) {
} else if (url.pathname.startsWith('/store/') || url.pathname.startsWith('/example/')) {
url.pathname = '/'
return await env.ASSETS.fetch(url, request)
} else if (url.pathname.startsWith('/info')) {
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
15 changes: 11 additions & 4 deletions src/frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { createSignal, onMount } from 'solid-js'
import Convert from 'ansi-to-html'
import Editor from './editor'
import Worker from './worker?worker'
import type { WorkerResponse, RunCode, File } from './types'
import type { WorkerResponse, RunCode, CodeFile } from './types'
import { Examples } from './examples'

const decoder = new TextDecoder()
const ansiConverter = new Convert()
Expand Down Expand Up @@ -53,7 +54,7 @@ export default function () {
}
})

async function runCode(files: File[], warmup: boolean = false) {
async function runCode(files: CodeFile[], warmup: boolean = false) {
setStatus('Launching Python...')
setInstalled('')
setOutputHtml('')
Expand All @@ -67,8 +68,14 @@ export default function () {
<main>
<header>
<h1>pydantic.run</h1>
<aside>Python browser sandbox.</aside>
<div id="counter"></div>
<aside>
Python browser sandbox,{' '}
<a href="https://github.com/pydantic/pydantic.run" target="_blank">
learn more
</a>
.
</aside>
<Examples />
</header>
<section>
<Editor runCode={runCode} />
Expand Down
68 changes: 23 additions & 45 deletions src/frontend/src/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { onMount, createSignal, Show } from 'solid-js'

import type * as monaco from 'monaco-editor'
import type { File } from './types'
import type { CodeFile } from './types'
import { retrieve, store } from './store.ts'
import { Tabs, findActive } from './tabs'
import type { Editor } from './monacoEditor'

interface EditorProps {
runCode: (files: File[], warmup?: boolean) => void
runCode: (files: CodeFile[], warmup?: boolean) => void
}

export default function ({ runCode }: EditorProps) {
Expand All @@ -15,51 +15,38 @@ export default function ({ runCode }: EditorProps) {
const [showSave, setShowSave] = createSignal(false)
const [showFork, setShowFork] = createSignal(false)
const [disableFork, setDisableFork] = createSignal(false)
const [files, setFiles] = createSignal<File[]>([])
const [files, setFiles] = createSignal<CodeFile[]>([])
const [fadeOut, setFadeOut] = createSignal(false)
let editor: monaco.editor.IStandaloneCodeEditor | null = null
let editor: Editor | null = null
const editorEl = (<div class="editor" />) as HTMLElement
let statusTimeout: number
let clearSaveTimeout: number
let clearForkTimeout: number

onMount(async () => {
const [{ monaco }, { files, allowSave, allowFork }] = await Promise.all([import('./monacoEditor'), retrieve()])
monaco.editor.defineTheme('custom-dark', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#1e1f2e',
},
})
const [{ Editor, KeyMod, KeyCode }, { files, allowSave, allowFork }] = await Promise.all([
import('./monacoEditor'),
retrieve(),
])

const active = findActive(files)
const file = files.find((f) => f.activeIndex === active)
editor = monaco.editor.create(editorEl, {
value: file ? file.content : '',
language: 'python',
theme: 'custom-dark',
automaticLayout: true,
minimap: {
enabled: false,
},
})
editor = new Editor(editorEl, file)

setFiles(files)
setShowSave(allowSave)
setShowFork(allowFork)
runCode(files, true)

editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, run)
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => save(updateFiles(getActiveContent()), true))
editor.addCommand(KeyMod.CtrlCmd | KeyCode.Enter, run)
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KeyS, () => save(updateFiles(editor!.getValue()), true))
editor.onDidChangeModelContent(() => {
clearTimeout(clearSaveTimeout)
clearSaveTimeout = setTimeout(() => save(updateFiles(getActiveContent())), 1200)
clearSaveTimeout = setTimeout(() => save(updateFiles(editor!.getValue())), 1200)
})
})

async function save(files: File[], verbose: boolean = false, fork: boolean = false) {
async function save(files: CodeFile[], verbose: boolean = false, fork: boolean = false) {
if (!saveActive()) {
return
}
Expand Down Expand Up @@ -90,7 +77,7 @@ export default function ({ runCode }: EditorProps) {
}
}

function updateFiles(activeContent: string): File[] {
function updateFiles(activeContent: string): CodeFile[] {
return setFiles((prev) => {
const active = findActive(prev)
return prev.map(({ name, content, activeIndex }) => {
Expand All @@ -100,49 +87,40 @@ export default function ({ runCode }: EditorProps) {
}

function run() {
const files = updateFiles(getActiveContent())
const files = updateFiles(editor!.getValue())
runCode(files)
save(files)
}

function getActiveContent(): string {
return editor!.getValue()
}

function setActiveContent(content: string) {
if (editor) {
editor.setValue(content)
}
}

function toggleSave(enabled: boolean) {
setSaveActive(enabled)
if (enabled) {
save(updateFiles(getActiveContent()), true)
save(updateFiles(editor!.getValue()), true)
}
}

function fork() {
setSaveActive(true)
save(updateFiles(getActiveContent()), true, true)
save(updateFiles(editor!.getValue()), true, true)
}

function addFile(name: string) {
// set active to 0, for new file, it'll be set by changeTab
const file: File = { name, content: '', activeIndex: 0 }
const file: CodeFile = { name, content: '', activeIndex: 0 }
setFiles((prev) => [...prev, file])
changeFile(name)
editor!.focus()
}

function changeFile(newName: string) {
const activeContent = getActiveContent()
const activeContent = editor!.getValue()
const files = setFiles((prev) => {
const active = findActive(prev)
return prev.map(({ name, content, activeIndex }) => {
if (name == newName) {
setActiveContent(content)
return { name, content, activeIndex: active + 1 }
const newFile = { name, content, activeIndex: active + 1 }
editor!.setFile(newFile)
return newFile
} else if (activeIndex === active) {
return { name, content: activeContent, activeIndex }
} else {
Expand Down
File renamed without changes.
78 changes: 78 additions & 0 deletions src/frontend/src/examples/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { CodeFile } from '../types'
import helloWorldExample from './hello_world.py?raw'
import logfireExample from './logfire_example.py?raw'
import pydanticExample from './pydantic_example.py?raw'

export function Examples() {
return (
<div class="examples">
{EXAMPLES.map(({ path, name }) => (
<a href={path}>{name}</a>
))}
</div>
)
}

export function getExample(path: string): CodeFile[] {
const urlExample = EXAMPLES.find((e) => e.path === path)
if (urlExample) {
return urlExample.files
} else {
return HELLO_WORLD_FILES
}
}

interface Example {
path: string
name: string
files: CodeFile[]
}

const HELLO_WORLD_FILES: CodeFile[] = [
{
name: 'main.py',
content: helloWorldExample,
activeIndex: 0,
},
]

const EXAMPLES: Example[] = [
{
path: '/example/blank',
name: 'Blank',
files: [
{
name: 'main.py',
content: '',
activeIndex: 0,
},
],
},
{
path: '/example/hello-world',
name: 'Hello world',
files: HELLO_WORLD_FILES,
},
{
path: '/example/logfire',
name: 'Pydantic Logfire',
files: [
{
name: 'main.py',
content: logfireExample,
activeIndex: 0,
},
],
},
{
path: '/example/pydantic',
name: 'Pydantic',
files: [
{
name: 'main.py',
content: pydanticExample,
activeIndex: 0,
},
],
},
]
8 changes: 8 additions & 0 deletions src/frontend/src/examples/logfire_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# /// script
# dependencies = ["https://githubproxy.samuelcolvin.workers.dev/samuelcolvin/scratch/blob/main/logfire-3.3.0-py3-none-any.whl"]
# ///

import logfire

logfire.configure(token='...')
logfire.info('Hello, {place}!', place='World')
26 changes: 26 additions & 0 deletions src/frontend/src/examples/pydantic_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from datetime import datetime

from pydantic import BaseModel, PositiveInt


class User(BaseModel):
id: int
name: str = 'John Doe'
signup_ts: datetime | None
tastes: dict[str, PositiveInt]


external_data = {
'id': 123,
'signup_ts': '2019-06-01 12:22',
'tastes': {
'wine': 9,
b'cheese': 7,
'cabbage': '1',
},
}

user = User(**external_data)

print('user id:', user.id)
print('user model_dump:', user.model_dump())
Loading

0 comments on commit 7f1a7f2

Please sign in to comment.