Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changes/unreleased/repl-repository-commands.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- **The REPL and the Jupyter kernel read and write a SysML v2 API repository with the OMG pilot kernel's four commands.** `%repo [<base path>]` shows or sets the server (seeded from `FLEXO_SYSMLV2_URL`, the token from `FLEXO_INTEROP_TOKEN`, never printed); `%projects` lists every project, paged to the end; `%load [--id=<project id>] [--name=<name>] [--branch=<branch name or id>] [<name>]` reads a project's model at a branch head into the session as notation and remembers where it came from; `%publish [-d] [--project=<project name>] [--branch=<branch name>] <name>` writes the elements rooted in a session element as a new project or as a commit of what changed on the branch, reporting the commit id and the counts created, updated and deleted; `-d` includes derived properties. Any server speaking the standard API serves — the pilot's `SysML-v2-API-Services` without a token, Flexo MMS with one — and the kernel answers argument problems as `UsageError`.
- **The standard SysML v2 API client is its own package.** `internal/translate/interop/sysmlapi` drives projects, branches, commits with `change` payloads, elements and roots with optional bearer authentication and follows a server's `Link: rel="next"` paging to the end; `internal/translate/interop/flexo` keeps Flexo's extras — Layer 1 Turtle, organizations, ETag pushes — over it, and `internal/translate/interop/modelsync` holds what `sysml -sync` and the REPL share.
7 changes: 7 additions & 0 deletions cmd/sysml/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !sysml_prod && !sysml_nosync

package main

import (
_ "github.com/Open-MBEE/OpenSysML/internal/frontend/repl/replext/repository" // registers the repository REPL commands
)
44 changes: 44 additions & 0 deletions docs/guide/12-jupyter.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,50 @@ The bounds the REPL takes from the environment (`OPENSYSML_MAX_STEPS`,
kernel, read when it starts: set them in the environment of the notebook server. See
[environment variables](../reference/environment.md).

## Working with a repository

The kernel round-trips models with a SysML v2 API server through the same four commands the
OMG pilot's kernel has: `%repo` shows or sets the server, `%projects` lists its projects,
`%load` reads a project's model into the session and `%publish` writes elements back as a new
project or a commit. Any server speaking the standard API serves: the pilot's
`SysML-v2-API-Services` wants no token and the default URL is its `http://localhost:9000`
when `%repo` names it; Flexo MMS wants a bearer token and an organization. Set, in the
environment of the notebook server:

| Variable | For the pilot API server | For Flexo MMS |
|---|---|---|
| `FLEXO_SYSMLV2_URL` | `http://localhost:9000` | the SysML v2 API endpoint, by default `http://localhost:8083` |
| `FLEXO_INTEROP_TOKEN` | unset | the bearer token; never put it in a cell |
| `FLEXO_SYSMLV2_ORG` | unset | the organization, by default `sysmlv2` |
| `FLEXO_ALLOW_PLAIN_HTTP` | unset on this machine | `1` to allow a plaintext `http://` server on another |

Then, as the pilot's notebooks do:

```text
%repo http://localhost:9000
%projects
%load --name=Vehicles --branch=main
part def Truck :> Vehicles::Car;
%publish --project=Vehicles Truck
%publish -d --project="Trucks only" --branch=main Truck
```

`%load` takes the project by name, by `--id` or by `--name`, the branch by name or id or the
default when absent, and submits the model as a document, so the cells after it refer to the
loaded names; the session remembers which project, branch and commit, and a `%publish` of
what it loaded is a commit on that branch, not a second project. `%publish <name>` names the
project after the element's own name unless `--project` says otherwise; a project of that name
receives a commit of what changed against the branch head, reported as the commit id with the
counts created, updated and deleted, and no project of that name is created. The commit stays
within its root: what the branch holds under other roots is left in place, and elements are
deleted only from a branch the session loaded or published, since only then has it seen them;
what was left alone is reported. `-d` sends every
derived property the exporter computes, as the pilot's `-d` does. An argument problem is a
`UsageError`; a server that cannot be reached, a missing project or branch, or a refused commit a
`CommandError` with the status and the server's message. A project name two projects share is
refused naming both ids: use `--id`. The [REPL commands reference](../reference/repl-commands.md#working-with-a-repository)
has each command's grammar.

## Troubleshooting

- **The kernel is not listed.** `jupyter kernelspec list` shows what the server sees; the
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/jupyter-kernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ kernel language is not `sysml` is refused. See
A `%quit` in a cell is noted — the notebook's shutdown ends the kernel — and an unknown
`%` command is an error (`UnknownCommand`), where the prompt would print guidance.

A `%` command given wrong arguments is a `UsageError` whose traceback is the command's usage;
a command that fails — a repository server that cannot be reached, a project that does not exist
— is a `CommandError` with the message the prompt would print, the HTTP status and the server's
message included.

The repository commands `%repo`, `%projects`, `%load` and `%publish` address the SysML v2 API
server `FLEXO_SYSMLV2_URL` names, with the bearer token `FLEXO_INTEROP_TOKEN` when the server wants
one and Flexo's organization `FLEXO_SYSMLV2_ORG`, all read from the kernel's environment; a token
is never echoed and no state is written. See [Working with a repository](../guide/12-jupyter.md#working-with-a-repository).

## Rich output

Output with a richer form than plain text is a `display_data` carrying that form beside
Expand Down
36 changes: 35 additions & 1 deletion docs/reference/repl-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ into the parts it holds (`car.fl.hub`, `#3.fl`, `car.wheels[2]`).
| `%list` | List all declarations in current session |
| `%clear` | Clear session (reset all declarations) |
| `%load [--cells <sel>] <path>...` | Submit the contents of files, directories or globs. A `.ipynb` notebook loads its code cells in order, each a source named `<notebook> cell <n>`; the cells' `%` command lines and expressions are skipped and counted in the report, a cell tagged `skip-load` is skipped, and `--cells 1,3-5` or `--cells tag:<tag>` picks cells by position among the code cells or by tag ([Reusing another notebook](../guide/12-jupyter.md#reusing-another-notebook)) |
| `%load [--id=<project id>] [--name=<name>] [--branch=<branch>] [<name>]` | Load a repository project's model into the session: the project named, or given by `--id` or `--name`, at the branch named by `--branch` (name or id) or its default branch. The elements at the branch head are read as SysML v2 API JSON and submitted as notation, so later input refers to the loaded names; the session remembers the project, branch and commit, and a `%publish` of the loaded element commits on that branch. A name two projects share is refused naming both ids. A lone name that is a path on disk — a separator, a `.sysml`/`.kerml`/`.ipynb` extension, a wildcard, or a file that exists — loads the files; a project whose name reads as one is loaded by `--name` ([Working with a repository](#working-with-a-repository)) |
| `%print [name]` | Print the session model as SysML notation at the prompt, or only the named element and its body (`%print 'My Pkg'::Car`). Comments are kept, since the same writer `%save` writes notation with is used, and what is printed can be typed back in. Notation only: nothing about RDF is reported. Reading the model materializes nothing and leaves a debugging session running |
| `%save <file>` | Write the session model to a file: `.sysml` notation (comments preserved), `.ttl` RDF or `.json` the API's element form, which are [experimental](rdf-mapping.md#status-experimental) and reported as such on each save |
| `%import <file> [as values] [map <file>] [format csv\|tsv\|json\|jsonl] [dry-run]` | Set the feature values a CSV, TSV, JSON or JSON Lines file assigns, one row per element: an `element` column names the element and each other column a feature, with a unit as `mass [kg]`. A declared value is replaced and an inherited one redefined; an empty cell leaves a value as it was. The file is checked whole first, and a row, value or unit the model refuses imports nothing. `map` names a JSON mapping file, `format` overrides the extension, and `dry-run` prints the values without changing the model. Same import as the CLI's [`-import`](cli.md#importing-data); see [Importing data into a model](../manual/importing-data.md) |
Expand Down Expand Up @@ -118,7 +119,15 @@ into the parts it holds (`car.fl.hub`, `#3.fl`, `car.wheels[2]`).
| `%advance <time>` | Advance the runtime's simulation clock by `<time>` seconds (`SI::s`), running every state event, action token, change-condition poll and do behavior that comes due, in due order. The clock is the session's, not one debugger's: an `%action` parked at `accept after` and a `%state` machine both move, and the report covers each. A state's do behavior whose action body waits (`do action poll { action wait accept after 3 [SI::s]; then … }`) is parked on the clock too, listed under `Waiting on the clock` and resumed when its instant comes; a transition that leaves the state first ends it, its wait leaving the clock. Two executors due at the same instant run in the order the scheduling policy picks — the one started last first under the default `reverse` — and the pick is a choice point, as is which of two regions' do behaviors due together acts first in a round ([Choice points](../guide/06-behavior.md)). Under `%engine check` there is no run to advance: `%advance` searches the behavior last checked again, up to that instant as its horizon |
| **Control** | |
| `%quit` | Exit the REPL |
| `Tab` | Complete meta commands, symbol names (after `%print`, `%instantiate`, `%features` …; a name that needs quoting is offered in quotes, `Q::'the ra` completing to `Q::'the rack'`), object references where a command takes one (`#` offers the ids there are; `car.` offers the object-holding features of `car` — the same ones a path may pass through — a multi-valued one as `car.wheels[1]`, `car.wheels[2]` …; completing reads and materializes nothing, so a part no command has reached yet is offered by type, and only the elements reading it would hold: those the features subsetting it contribute, then anonymous ones up to its lower bound — so an optional part (`spare : Wheel[0..1]`) or an abstract one, which hold only what subsets them, is offered only once something does), the form after `%render <name>` and the palette after `%render <name> dot`, and file paths after `%load` and `%save` |

**Repository** — the SysML v2 API server the session talks to ([Working with a repository](#working-with-a-repository)):

| Command | Description |
|---------|-------------|
| `%repo [<base path>]` | Show or set the base URL of the SysML v2 API server the repository commands address, seeded from `FLEXO_SYSMLV2_URL` (default `http://localhost:8083`). A bearer token, when the server wants one, is read from `FLEXO_INTEROP_TOKEN`; it is never printed, and nothing is persisted. A plaintext `http://` URL off this machine is refused unless `FLEXO_ALLOW_PLAIN_HTTP=1` |
| `%projects` | List the repository's projects, `<name> (<id>)` one per line, every page of them; `no projects` for an empty repository |
| `%publish [-d] [--project=<project name>] [--branch=<branch name>] <name>` | Publish the elements rooted in `<name>` — a qualified name resolved in the session — as SysML v2 API JSON: when no project is named after it (or after `--project`) a project is created and its first commit made, otherwise a commit on the branch `--branch` names, or the default branch, holding what differs from the branch head under that root — elements under other roots are left in place, and elements are deleted only from a branch the session loaded or published: the commit id and the counts of elements created, updated and deleted are reported, with what was left in place, `nothing to publish` when none differ. An option given twice is a usage error. `-d` sends every derived property the exporter computes; without it the ones the reader recomputes are left out. A project with the same name twice is refused naming both ids |
| `Tab` | Complete meta commands, symbol names (after `%print`, `%instantiate`, `%features` …; a name that needs quoting is offered in quotes, `Q::'the ra` completing to `Q::'the rack'`), object references where a command takes one (`#` offers the ids there are; `car.` offers the object-holding features of `car` — the same ones a path may pass through — a multi-valued one as `car.wheels[1]`, `car.wheels[2]` …; completing reads and materializes nothing, so a part no command has reached yet is offered by type, and only the elements reading it would hold: those the features subsetting it contribute, then anonymous ones up to its lower bound — so an optional part (`spare : Wheel[0..1]`) or an abstract one, which hold only what subsets them, is offered only once something does), the form after `%render <name>` and the palette after `%render <name> dot`, file paths after `%load` and `%save`, and the flags of `%load` and `%publish` with the repository's project names after `--name=`, `--id=` and `--project=` and after `%load` |
| `Ctrl-C` | Discard the line being typed, and any continuation buffered before it; at an empty `sysml>` prompt, exit REPL |
| `Ctrl-D` | Exit REPL |

Expand Down Expand Up @@ -625,3 +634,28 @@ pins no positions either, so `DiagramLayout` geometry rides along as `#` comment
([the D2 form](../project/view-rendering-forms.md#d2)).

To render a view outside the prompt, use [`sysml -render`](cli.md#rendering-a-view).

## Working with a repository

`%repo`, `%projects`, `%load` and `%publish` address a SysML v2 API server — the OMG pilot's
`SysML-v2-API-Services` or Flexo MMS — through the standard `/projects`, `/branches`, `/commits`
and `/elements` resources, as the CLI's [`-sync`](cli.md#reading-and-pushing-a-repository-branch)
does. The server is `FLEXO_SYSMLV2_URL` until `%repo <url>` sets another for the session; a
bearer token comes from `FLEXO_INTEROP_TOKEN` and is never shown, and Flexo's organization from
`FLEXO_SYSMLV2_ORG` where a server has one. Nothing is written to disk.

```text
sysml> %repo http://localhost:9000
API base path: http://localhost:9000
sysml> %projects
Vehicles (9c0e2b3a-…)
sysml> %load Vehicles
loaded project Vehicles (9c0e2b3a-…) at branch main (5d1f…), commit 7a2c…: 13 elements
sysml> part def Truck :> Vehicles::Car;
sysml> %publish --project=Vehicles Truck
commit 0b4e… on branch main (5d1f…) of Vehicles (9c0e2b3a-…): 3 created, 0 updated, 0 deleted
```

A server that cannot be reached, a project or branch that does not exist, and a commit the server
refuses are reported with the status and the server's message; a syntax or argument problem with
the command's usage.
4 changes: 4 additions & 0 deletions internal/frontend/jupyter/replengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ func (e *REPLEngine) meta(line string, out Output) error {
}
lines, quit, err := e.session.RunMeta(line)
if err != nil {
var usage *repl.UsageError
if errors.As(err, &usage) {
return execError("UsageError", usage.Error(), usage.Lines)
}
name := "CommandError"
if errors.Is(err, runtime.ErrInterrupted) {
name = "KeyboardInterrupt"
Expand Down
48 changes: 48 additions & 0 deletions internal/frontend/jupyter/repository_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package jupyter

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

// TestRepositoryCommandsRunThroughTheKernel drives %repo and %projects against
// a fake SysML v2 API server: the listing streams as text, an argument problem
// is a UsageError, and a server failure a CommandError naming the status.
func TestRepositoryCommandsRunThroughTheKernel(t *testing.T) {
var projects []map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/projects" {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"error":"the database is away"}`))
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(projects)
}))
defer server.Close()
t.Setenv("FLEXO_SYSMLV2_URL", server.URL)
t.Setenv("FLEXO_INTEROP_TOKEN", "")

e := newEngine(t)
if got := mustRun(t, e, "%repo").text(); got != "stdout: API base path: "+server.URL+"\n" {
t.Errorf("%%repo streamed %q", got)
}
if got := mustRun(t, e, "%projects").text(); got != "stdout: no projects\n" {
t.Errorf("%%projects over an empty repository streamed %q", got)
}
projects = []map[string]any{{"@id": "p-1", "name": "Rover", "defaultBranch": map[string]any{"@id": "b-1"}}}
if got := mustRun(t, e, "%projects").text(); got != "stdout: Rover (p-1)\n" {
t.Errorf("%%projects streamed %q", got)
}

if failure := mustFail(t, e, "%projects now"); failure.Name != "UsageError" || !strings.HasPrefix(failure.Value, "usage: %projects") {
t.Errorf("%%projects now: %s: %s", failure.Name, failure.Value)
}
failure := mustFail(t, e, "%load --id=p-1")
if failure.Name != "CommandError" || !strings.Contains(failure.Value, "Internal Server Error") || !strings.Contains(failure.Value, "the database is away") {
t.Errorf("a server failure: %s: %s", failure.Name, failure.Value)
}
}
5 changes: 5 additions & 0 deletions internal/frontend/repl/complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ func (s *Session) Complete(line string, pos int) Completion {
if word := lastField(head); strings.HasPrefix(word, "%") && word == command {
return completion(word, matchingPrefix(metaCommands(), word))
}
if command == "%load" || command == "%publish" {
if candidates, ok := s.repositoryCompletions(command, lastField(head)); ok {
return completion(lastField(head), candidates)
}
}
if command == "%load" || command == "%save" {
word := lastField(head)
return completion(word, pathCompletions(word))
Expand Down
Loading
Loading