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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ __dev/
# Agents
.claude/
CLAUDE.md
AGENTS.md
AGENTS.md
SKILL.md
8 changes: 4 additions & 4 deletions julia/RtemisA3/src/api.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ function create_a3(
variant !== nothing && (annot["variant"] = variant)

raw = Dict{String,Any}(
"\$schema" => _A3_SCHEMA_URI,
"a3_version" => _A3_VERSION,
"sequence" => sequence,
"\$schema" => _A3_SCHEMA_URI,
"a3_version" => _A3_VERSION,
"sequence" => sequence,
"annotations" => annot,
"metadata" => metadata !== nothing ? metadata : Dict{String,Any}(),
)
metadata !== nothing && (raw["metadata"] = metadata)
A3(raw)
end

Expand Down
8 changes: 6 additions & 2 deletions julia/RtemisA3/src/validate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,14 @@ function A3(raw::AbstractDict)
end
haskey(raw, "sequence") ||
throw(A3ValidationError("missing required field 'sequence'"))
haskey(raw, "annotations") ||
throw(A3ValidationError("missing required field 'annotations'"))
haskey(raw, "metadata") ||
throw(A3ValidationError("missing required field 'metadata'"))

seq = validate_sequence(raw["sequence"], "sequence")
annotations = parse_annotations(get(raw, "annotations", Dict{String,Any}()), "annotations")
metadata = parse_metadata(get(raw, "metadata", Dict{String,Any}()), "metadata")
annotations = parse_annotations(raw["annotations"], "annotations")
metadata = parse_metadata(raw["metadata"], "metadata")

validate_bounds(seq, annotations)
A3(seq, annotations, metadata)
Expand Down
6 changes: 6 additions & 0 deletions python/rtemis_a3/src/rtemis/a3/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ def a3_from_json(text: str) -> A3:
raise A3ParseError(
f"'a3_version' must be '{_A3_VERSION}', got '{version_val}'"
)
if "sequence" not in data:
raise A3ParseError("missing required field 'sequence'")
if "annotations" not in data:
raise A3ParseError("missing required field 'annotations'")
if "metadata" not in data:
raise A3ParseError("missing required field 'metadata'")
# Strip envelope keys before passing to the data model
for key in _ENVELOPE_KEYS:
data.pop(key, None)
Expand Down
16 changes: 14 additions & 2 deletions python/rtemis_a3/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_bounds_error(self):
# a3_from_json / a3_to_json
# ---------------------------------------------------------------------------

MINIMAL_JSON = '{"$schema": "https://schema.rtemis.org/a3/v1/schema.json", "a3_version": "1.0.0", "sequence": "MAEPRQ"}'
MINIMAL_JSON = '{"$schema": "https://schema.rtemis.org/a3/v1/schema.json", "a3_version": "1.0.0", "sequence": "MAEPRQ", "annotations": {}, "metadata": {}}'

FULL_JSON = """{
"$schema": "https://schema.rtemis.org/a3/v1/schema.json",
Expand Down Expand Up @@ -104,9 +104,21 @@ def test_invalid_json(self):
def test_valid_json_invalid_a3(self):
with pytest.raises(A3ValidationError):
a3_from_json(
'{"$schema": "https://schema.rtemis.org/a3/v1/schema.json", "a3_version": "1.0.0", "sequence": "M"}'
'{"$schema": "https://schema.rtemis.org/a3/v1/schema.json", "a3_version": "1.0.0", "sequence": "M", "annotations": {}, "metadata": {}}'
) # too short

def test_missing_annotations_field(self):
with pytest.raises(A3ParseError, match="annotations"):
a3_from_json(
'{"$schema": "https://schema.rtemis.org/a3/v1/schema.json", "a3_version": "1.0.0", "sequence": "MAEPRQ", "metadata": {}}'
)

def test_missing_metadata_field(self):
with pytest.raises(A3ParseError, match="metadata"):
a3_from_json(
'{"$schema": "https://schema.rtemis.org/a3/v1/schema.json", "a3_version": "1.0.0", "sequence": "MAEPRQ", "annotations": {}}'
)

def test_missing_schema_field(self):
with pytest.raises(A3ParseError, match=r"\$schema"):
a3_from_json('{"a3_version": "1.0.0", "sequence": "MAEPRQ"}')
Expand Down
1 change: 0 additions & 1 deletion r/.Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,5 @@
^docs$
^AGENTS\.md$
^CLAUDE\.md$
^NEWS\.md$
^pkgdown$
^SKILL\.md$
20 changes: 10 additions & 10 deletions r/DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
Package: rtemis.a3
Title: Amino Acid Annotation (A3) format
Version: 0.5.1
Date: 2026-03-29
Authors@R:
person(given = "E.D.", family = "Gennatas", role = c("aut", "cre"),
Title: Amino Acid Annotation (A3) Format
Version: 0.5.2
Date: 2026-04-03
Authors@R:
person(given = "E.D.", family = "Gennatas", role = c("aut", "cre", "cph"),
email = "gennatas@gmail.com",
comment = c(ORCID = "0000-0001-9280-3609"))
Description: Defines the annotated amino acid (A3) format using S7 classes. Provides functions to
create A3 objects, read and write A3 JSON files.
URL: https://rtemis.a3.rtemis.org
Description: Implements the Amino Acid Annotation (A3) format using 'S7' classes.
The A3 format is a structured 'JSON' schema for annotating amino acid sequences
with site, region, post-translational modification (PTMs), processing event,
and sequence variant annotations. Provides functions to create, read, and write A3 objects.
URL: https://a3.rtemis.org
License: GPL (>= 3)
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
Expand All @@ -22,8 +24,6 @@ Imports:
utils
Suggests:
biomaRt,
colorspace,
dplyr,
httr,
jsonlite,
seqinr,
Expand Down
17 changes: 17 additions & 0 deletions r/NEWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# rtemis.a3 0.5.2

## New features

* Initial CRAN release.
* Defines the Amino Acid Annotation (A3) format using S7 classes.
* Core classes: `A3`, `A3Sequence`, `A3Annotation`, `A3Metadata`, `A3Site`,
`A3Region`, `A3PTM`, `A3Processing`, `A3Variant`.
* `create_A3()`: create A3 objects with full validation.
* `annotation_position()`, `annotation_range()`, `annotation_variant()`:
helpers to build annotation entries.
* `write_A3json()` / `read_A3json()`: serialize and deserialize A3 objects to
and from JSON files.
* `concat()`: concatenate a character vector into a single sequence string.
* Database utilities: `uniprot_to_A3()`, `uniprot_sequence()`,
`gene2sequence()`, `get_alphafold()`, `pdb_annotations()`,
`clinvar_variants()`.
33 changes: 17 additions & 16 deletions r/R/0_init.R
Original file line number Diff line number Diff line change
Expand Up @@ -724,16 +724,16 @@ format_caller <- function(call_stack, call_depth, caller_id, max_char = 30L) {
# \code{current <- as.list(sys.call())[[1]]}
#'
#' @param ... Message to print
#' @param date Logical: if TRUE, include date and time in the prefix
#' @param caller Character: Name of calling function
#' @param call_depth Integer: Print the system call path of this depth.
#' @param caller_id Integer: Which function in the call stack to print
#' @param newline_pre Logical: If TRUE begin with a new line.
#' @param newline Logical: If TRUE end with a new line.
#' @param format_fn Function: Formatting function to use on the message text.
#' @param sep Character: Use to separate objects in `...`
#' @param verbosity Integer: Verbosity level.
#'
#' @return Invisibly: List with call, message, and date
#' @return Invisibly: `NULL`
#'
#' @author EDG
#' @noRd
Expand All @@ -742,27 +742,28 @@ format_caller <- function(call_stack, call_depth, caller_id, max_char = 30L) {
#' msg("Hello, world!")
msg <- function(
...,
date = TRUE,
caller = NULL,
call_depth = 1L,
caller_id = 1L,
newline_pre = FALSE,
newline = TRUE,
format_fn = plain,
sep = " "
sep = " ",
verbosity = 1L
) {
if (verbosity == 0L) {
return(invisible(NULL))
}
if (is.null(caller)) {
call_stack <- as.list(sys.calls())
caller <- format_caller(call_stack, call_depth, caller_id)
}
} # / get caller

txt <- Filter(Negate(is.null), list(...))
if (newline_pre) {
message("")
}
if (date) {
msgdatetime()
}
msgdatetime()
message(
format_fn(paste(txt, collapse = sep)),
appendLF = FALSE
Expand Down Expand Up @@ -791,8 +792,12 @@ msg0 <- function(
newline_pre = FALSE,
newline = TRUE,
format_fn = plain,
sep = ""
sep = "",
verbosity = 1L
) {
if (verbosity == 0L) {
return(invisible(NULL))
}
if (is.null(caller)) {
call_stack <- as.list(sys.calls())
caller <- format_caller(call_stack, call_depth, caller_id)
Expand Down Expand Up @@ -828,13 +833,9 @@ msg0 <- function(
#' @noRd
#'
#' @examples
#' \dontrun{
#' {
#' msg("Hello")
#' pcat("super", "wow")
#' pcat(NULL, "oooo")
#' }
#' }
#' msg("Hello")
#' pcat("super", "potato")
#' pcat(NULL, "oooo")
pcat <- function(left, right, pad = 17, newline = TRUE) {
lpad <- max(0, pad - 1 - max(0, nchar(left)))
cat(pad_string(left), right)
Expand Down
9 changes: 9 additions & 0 deletions r/R/a3.R
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,15 @@ A3from_json <- function(x, ...) {
"Field {.field a3_version} must be {.val {.A3_VERSION}}, got {.val {version_field}}."
)
}
if (is.null(x[["sequence"]])) {
cli::cli_abort("JSON input missing required field {.field sequence}.")
}
if (is.null(x[["annotations"]])) {
cli::cli_abort("JSON input missing required field {.field annotations}.")
}
if (is.null(x[["metadata"]])) {
cli::cli_abort("JSON input missing required field {.field metadata}.")
}

sequence <- x[["sequence"]]
annotations <- x[["annotations"]]
Expand Down
19 changes: 9 additions & 10 deletions r/R/gene2sequence.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#'
#' @examples
#' \dontrun{
#' mapt_seqs <- gene2sequence("MAPT")
#' mapt_seq <- gene2sequence("MAPT")
#' }
gene2sequence <- function(
gene,
Expand Down Expand Up @@ -50,15 +50,14 @@ gene2sequence <- function(
mart = mart
)

if (verbosity > 0) {
msg0(
"Found ",
bold(nrow(transcripts)),
" transcripts for gene ",
highlight(gene),
"."
)
}
msg0(
"Found ",
bold(nrow(transcripts)),
" transcripts for gene ",
highlight(gene),
".",
verbosity = verbosity
)

# Get sequence ----
# Retrieve sequence(s) using transcript ID
Expand Down
31 changes: 1 addition & 30 deletions r/R/rtemis_color_system.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,39 +27,10 @@ rt_teal <- rtemis_teal
rt_purple <- rtemis_purple
rt_magenta <- rtemis_light_magenta
highlight_col <- coastside_orange

col_object <- rt_green


#' rtemis Color System
#'
#' A named list of colors used consistently across all packages
#' in the rtemis ecosystem.
#'
#' Colors are provided as hex strings.
#'
#' @format A named list with the following elements:
#' \describe{
#' \item{red}{"kaimana red"}
#' \item{blue}{"kaimana light blue"}
#' \item{green}{"kaimana medium green"}
#' \item{orange}{"coastside orange"}
#' \item{teal}{"rtemis teal"}
#' \item{purple}{"rtemis purple"}
#' \item{magenta}{"rtemis magenta"}
#' \item{highlight_col}{"highlight color"}
#' \item{object}{"rtemis teal"}
#' \item{info}{"lmd burgundy"}
#' \item{outer}{"kaimana red"}
#' \item{tuner}{"coastside orange"}
#' }
#'
#' @author EDG
#'
#' @noRd
#'
#' @examples
#' rtemis_colors[["teal"]]
# %% rtemis_colors ----
rtemis_colors <- list(
red = kaimana_red,
light_blue = kaimana_light_blue,
Expand Down
23 changes: 10 additions & 13 deletions r/R/utils_a3.R
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,17 @@ aa_sub <- function(x, substitutions, verbosity = 1L) {
from <- strngs[1]
to <- strngs[length(strngs)]
pos <- as.numeric(strngs[2:(length(strngs) - 1)] |> paste(collapse = ""))
if (verbosity > 0) {
msg(
"Substituting",
highlight(from),
"at position",
highlight(pos),
"with",
highlight(to)
)
}
msg(
"Substituting",
highlight(from),
"at position",
highlight(pos),
"with",
highlight(to),
verbosity = verbosity
)
x[pos] <- to
}
if (verbosity > 0) {
msg("All done.")
}
msg("All done.", verbosity = verbosity)
x
Comment thread
egenn marked this conversation as resolved.
}
Loading
Loading