From 50d8a283a2baf2aff61ef24016a220a2a56e2227 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 25 Feb 2026 15:48:58 +0000 Subject: [PATCH 1/4] Refactored topic creation/config to utilities class --- phoebus-product/pom.xml | 6 + .../alarm/server/AlarmServerMain.java | 594 ++++++------------ .../applications/alarm/server/TopicUtils.java | 131 ++++ 3 files changed, 343 insertions(+), 388 deletions(-) create mode 100644 services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java diff --git a/phoebus-product/pom.xml b/phoebus-product/pom.xml index e4747da801..2eeea467e9 100644 --- a/phoebus-product/pom.xml +++ b/phoebus-product/pom.xml @@ -57,6 +57,12 @@ app-logbook-inmemory 5.0.3-SNAPSHOT + + org.phoebus + app-utility-preference-manager + 5.0.3-SNAPSHOT + + org.phoebus app-logbook-olog-ui diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java index 45cc1ba0eb..1a5b750800 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java @@ -7,7 +7,17 @@ *******************************************************************************/ package org.phoebus.applications.alarm.server; -import static org.phoebus.applications.alarm.AlarmSystemConstants.logger; +import com.fasterxml.jackson.databind.JsonNode; +import org.phoebus.applications.alarm.AlarmSystemConstants; +import org.phoebus.applications.alarm.client.ClientState; +import org.phoebus.applications.alarm.model.AlarmTreeItem; +import org.phoebus.applications.alarm.model.AlarmTreeLeaf; +import org.phoebus.applications.alarm.model.SeverityLevel; +import org.phoebus.applications.alarm.model.json.JsonModelReader; +import org.phoebus.applications.alarm.model.json.JsonTags; +import org.phoebus.applications.alarm.model.print.ModelPrinter; +import org.phoebus.framework.preferences.PropertyPreferenceLoader; +import org.phoebus.util.shell.CommandShell; import java.io.FileInputStream; import java.util.ArrayList; @@ -18,187 +28,63 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.prefs.Preferences; -import org.apache.kafka.clients.admin.AdminClient; -import org.apache.kafka.clients.admin.AdminClientConfig; -import org.apache.kafka.clients.admin.AlterConfigOp; -import org.apache.kafka.clients.admin.ConfigEntry; -import org.apache.kafka.clients.admin.NewTopic; -import org.phoebus.applications.alarm.AlarmSystemConstants; -import org.phoebus.applications.alarm.client.ClientState; -import org.phoebus.applications.alarm.client.KafkaHelper; -import org.phoebus.applications.alarm.model.AlarmTreeItem; -import org.phoebus.applications.alarm.model.AlarmTreeLeaf; -import org.phoebus.applications.alarm.model.SeverityLevel; -import org.phoebus.applications.alarm.model.json.JsonModelReader; -import org.phoebus.applications.alarm.model.json.JsonTags; -import org.phoebus.applications.alarm.model.print.ModelPrinter; -import org.phoebus.framework.preferences.PropertyPreferenceLoader; -import org.phoebus.util.shell.CommandShell; - -import com.fasterxml.jackson.databind.JsonNode; -import org.apache.kafka.common.config.ConfigResource; +import static org.phoebus.applications.alarm.AlarmSystemConstants.logger; -/** Alarm Server - * @author Kay Kasemir +/** + * Alarm Server + * + * @author Kay Kasemir */ @SuppressWarnings("nls") -public class AlarmServerMain implements ServerModelListener -{ +public class AlarmServerMain implements ServerModelListener { private final SynchronousQueue restart = new SynchronousQueue<>(); - private volatile ServerModel model; + private volatile ServerModel model; private volatile CommandShell shell; private String current_path = ""; private static final String COMMANDS = - "Commands:\n\n" + - "Note: '.' and '..' will be interpreted as the current directory and the parent directory respectively.\n" + - "Spaces within a path do not need to be quoted.\n\n" + - "\tls - List all alarm tree items in the current directory.\n" + - "\tls -disconnected - List all the disconnected PVs in the entire alarm tree.\n" + - "\tls -disabled - List all the disabled PVs in the entire alarm tree.\n" + - "\tls -all - List all alarm tree PVs in the entire alarm tree.\n" + - "\tls -active - .. which are in active alarm.\n" + - "\tls -alarm - .. alarm, active or acknowledged.\n" + - "\tls dir - List all alarm tree items in the specified directory contained in the current directory.\n" + - "\tls /path/to/dir - List all alarm tree items in the specified directory at the specified path.\n" + - "\tcd - Change to the root directory.\n" + - "\tcd dir - Change to the specified directory contained in the current directory.\n" + - "\tcd /path/to/dir - Change to the specified directory at the specified path.\n" + - "\tpv pv - Print the specified PV in the current directory.\n" + - "\tpv /path/to/pv - Print the specified PV at the specified path.\n" + - "\tmode - Show mode.\n" + - "\tmode normal - Select normal mode.\n" + - "\tmode maintenance - Select maintenance mode.\n" + - "\tresend - Re-send all PV states to clients (for tests after network issues).\n" + - "\trestart - Re-load alarm configuration and restart.\n" + - "\tshutdown - Shut alarm server down and exit.\n"; - - /** - * Ensure that the required Kafka topics exist and are correctly configured. - *

- * Creates and configures the main alarm topic (compacted) and command/talk topics (deleted). - * For more details on alarm topic configuration, see: - * Refer to Configure Alarm Topics - * - * @param server Kafka server - * @param topic Base topic name - * @param kafka_props_file Extra Kafka properties file - * @throws Exception - */ - private static void ensureKafkaTopics(String server, String topic, String kafka_props_file) throws Exception { - var kafka_props = KafkaHelper.loadPropsFromFile(kafka_props_file); - kafka_props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, server); - try (AdminClient admin = AdminClient.create(kafka_props)) { - Set topics = admin.listTopics().names().get(60, TimeUnit.SECONDS); - // Compacted topic - String compactedTopic = topic; - if (!topics.contains(compactedTopic)) { - createTopic(admin, compactedTopic); - } - setCompactedConfig(admin, compactedTopic); - - // Deleted topics - for (String suffix : List.of("Command", "Talk")) { - String deletedTopic = topic + suffix; - if (!topics.contains(deletedTopic)) { - createTopic(admin, deletedTopic); - } - setDeletedConfig(admin, deletedTopic); - } - } - } - - /** - * Create topics - * - * @param admin Admin client - * @param topic Topic name - * @throws Exception - */ - private static void createTopic(AdminClient admin, String topic) throws Exception { - NewTopic newTopic = new NewTopic(topic, 1, (short) 1); - try { - admin.createTopics(List.of(newTopic)).all().get(); - logger.info("Created topic: " + topic); - } catch (Exception e) { - if (e.getCause() instanceof org.apache.kafka.common.errors.TopicExistsException) { - logger.info("Topic already exists: " + topic); - } else { - throw e; - } - } - } - - /** - * Configure topic for alarm state storage with compaction to retain latest state. - * For configuration information, see: - *

- * Refer to Configure Alarm Topics - * - * @param admin Admin client - * @param topic Topic name - * @throws Exception - */ - private static void setCompactedConfig(AdminClient admin, String topic) throws Exception { - ConfigResource resource = new ConfigResource(ConfigResource.Type.TOPIC, topic); - List configOps = List.of( - new AlterConfigOp(new ConfigEntry("cleanup.policy", "compact"), AlterConfigOp.OpType.SET), - new AlterConfigOp(new ConfigEntry("segment.ms", "10000"), AlterConfigOp.OpType.SET), - new AlterConfigOp(new ConfigEntry("min.cleanable.dirty.ratio", "0.01"), AlterConfigOp.OpType.SET), - new AlterConfigOp(new ConfigEntry("min.compaction.lag.ms", "1000"), AlterConfigOp.OpType.SET) - ); - admin.incrementalAlterConfigs(Map.of(resource, configOps)).all().get(); - logger.info("Set compacted config for topic: " + topic); - } - - /** - * Configure topic for command/talk messages with time-based deletion. - * For configuration information, see: - * - * Refer to Configure Alarm Topics - * - * @param admin Admin client - * @param topic Topic name - * @throws Exception - */ - private static void setDeletedConfig(AdminClient admin, String topic) throws Exception { - ConfigResource resource = new ConfigResource(ConfigResource.Type.TOPIC, topic); - List configOps = List.of( - new AlterConfigOp(new ConfigEntry("cleanup.policy", "delete"), AlterConfigOp.OpType.SET), - new AlterConfigOp(new ConfigEntry("segment.ms", "10000"), AlterConfigOp.OpType.SET), - new AlterConfigOp(new ConfigEntry("min.cleanable.dirty.ratio", "0.01"), AlterConfigOp.OpType.SET), - new AlterConfigOp(new ConfigEntry("min.compaction.lag.ms", "1000"), AlterConfigOp.OpType.SET), - new AlterConfigOp(new ConfigEntry("retention.ms", "20000"), AlterConfigOp.OpType.SET), - new AlterConfigOp(new ConfigEntry("delete.retention.ms", "1000"), AlterConfigOp.OpType.SET), - new AlterConfigOp(new ConfigEntry("file.delete.delay.ms", "1000"), AlterConfigOp.OpType.SET) - ); - admin.incrementalAlterConfigs(Map.of(resource, configOps)).all().get(); - logger.info("Set deleted config for topic: " + topic); - } - - private AlarmServerMain(final String server, final String config, final boolean use_shell, final String kafka_props_file) - { + "Commands:\n\n" + + "Note: '.' and '..' will be interpreted as the current directory and the parent directory respectively.\n" + + "Spaces within a path do not need to be quoted.\n\n" + + "\tls - List all alarm tree items in the current directory.\n" + + "\tls -disconnected - List all the disconnected PVs in the entire alarm tree.\n" + + "\tls -disabled - List all the disabled PVs in the entire alarm tree.\n" + + "\tls -all - List all alarm tree PVs in the entire alarm tree.\n" + + "\tls -active - .. which are in active alarm.\n" + + "\tls -alarm - .. alarm, active or acknowledged.\n" + + "\tls dir - List all alarm tree items in the specified directory contained in the current directory.\n" + + "\tls /path/to/dir - List all alarm tree items in the specified directory at the specified path.\n" + + "\tcd - Change to the root directory.\n" + + "\tcd dir - Change to the specified directory contained in the current directory.\n" + + "\tcd /path/to/dir - Change to the specified directory at the specified path.\n" + + "\tpv pv - Print the specified PV in the current directory.\n" + + "\tpv /path/to/pv - Print the specified PV at the specified path.\n" + + "\tmode - Show mode.\n" + + "\tmode normal - Select normal mode.\n" + + "\tmode maintenance - Select maintenance mode.\n" + + "\tresend - Re-send all PV states to clients (for tests after network issues).\n" + + "\trestart - Re-load alarm configuration and restart.\n" + + "\tshutdown - Shut alarm server down and exit.\n"; + + private AlarmServerMain(final String server, final String config, final boolean use_shell, final String kafka_props_file) { logger.info("Server: " + server); logger.info("Config: " + config); logger.info("Extra Kafka Properties: " + kafka_props_file); - try - { + try { // 'main' loop that keeps performing a full startup and shutdown // whenever a 'restart' is requested. boolean run = true; - while (run) - { + while (run) { logger.info("Verify topics exists and are correctly configured..."); // Create/verify topics before using Kafka - ensureKafkaTopics(server, config, kafka_props_file); + TopicUtils.ensureKafkaTopics(server, config, kafka_props_file); logger.info("Fetching past alarm states..."); final AlarmStateInitializer init = new AlarmStateInitializer(server, config, kafka_props_file); @@ -212,8 +98,7 @@ private AlarmServerMain(final String server, final String config, final boolean model = new ServerModel(server, config, initial_states, this, kafka_props_file); model.start(); - if (use_shell) - { + if (use_shell) { shell = new CommandShell(COMMANDS, this::handleShellCommands); // Start the command shell at the root node. @@ -235,9 +120,7 @@ private AlarmServerMain(final String server, final String config, final boolean model.shutdown(); } - } - catch (final Throwable ex) - { + } catch (final Throwable ex) { logger.log(Level.SEVERE, "Alarm Server main loop error", ex); } @@ -248,16 +131,15 @@ private AlarmServerMain(final String server, final String config, final boolean /** * Handle shell commands. Passed to command shell. + * * @param args - variadic String * @return result - boolean result of executing the command. * @throws Throwable */ - private boolean handleShellCommands(final String... args) throws Throwable - { + private boolean handleShellCommands(final String... args) throws Throwable { if (args == null) restart.offer(false); - else if (args.length == 1) - { + else if (args.length == 1) { if (args[0].startsWith("shut")) restart.offer(false); else if (args[0].equals("restart")) @@ -273,18 +155,14 @@ else if (args[0].equals("cd")) // cd with no argument goes to root directory. { current_path = model.getRoot().getPathName(); shell.setPrompt(current_path); - } - else if (args[0].equals("ls")) // List alarm tree items in current directory. _Not_ recursive descent. + } else if (args[0].equals("ls")) // List alarm tree items in current directory. _Not_ recursive descent. { List> children = model.findNode(current_path).getChildren(); for (final AlarmTreeItem child : children) System.out.println(child.getName() + " - " + child.getState()); - } - else + } else return false; - } - else if (args.length >= 2) - { + } else if (args.length >= 2) { // Concatenate all the tokens whose index is > 0 into a single string. // This allows for spaces in PV and Node names. They would have been split on whitespace by the CommandShell. String args1 = ""; @@ -292,30 +170,26 @@ else if (args.length >= 2) args1 += " " + args[i]; args1 = args1.trim(); - try - { + try { if (args[0].equals("cd")) // Change directory to specified location. { final String new_path = determinePath(args1); final AlarmTreeItem new_loc = model.findNode(new_path); - if (null == new_loc) - { + if (null == new_loc) { System.out.println("Node not found: " + new_path); return false; } // Can't change location to leaves. - if (new_loc instanceof AlarmTreeLeaf) - { + if (new_loc instanceof AlarmTreeLeaf) { System.out.println("Node not a directory: " + new_loc.getPathName()); return false; } current_path = new_loc.getPathName(); shell.setPrompt(current_path); - } - else if (args[0].equals("ls")) // List the alarm tree items at the specified location. + } else if (args[0].equals("ls")) // List the alarm tree items at the specified location. { if (args1.startsWith("-disc")) // Print all disconnected PVs in tree. listPVs(model.getRoot(), PVMode.Disconnected); @@ -332,8 +206,7 @@ else if (args1.startsWith("-ala")) // Print all the PVs in the tree that are in final String path = determinePath(args1); final AlarmTreeItem node = model.findNode(path); - if (null == node) - { + if (null == node) { System.out.println("Node not found: " + path); return false; } @@ -342,32 +215,26 @@ else if (args1.startsWith("-ala")) // Print all the PVs in the tree that are in for (final AlarmTreeItem child : children) System.out.println(child.getName() + " - " + child.getState()); } - } - else if (args[0].equals("pv")) // Print the specified PV. + } else if (args[0].equals("pv")) // Print the specified PV. { final String pvPath = determinePath(args1); final AlarmTreeItem node = model.findNode(pvPath); - if (node instanceof AlarmServerNode) - { + if (node instanceof AlarmServerNode) { System.out.println("Specified alarm tree item is not a PV: " + pvPath); return false; } final AlarmServerPV pv = (AlarmServerPV) node; System.out.println(pv); - } - else if (args[0].equals("mode")) - { + } else if (args[0].equals("mode")) { setMaintenanceMode(args1.startsWith("maint")); System.out.println(AlarmLogic.getMaintenanceMode() ? "Maintenance mode" : "Normal mode"); } } // Catch the exceptions caused by findNode searching a path that doesn't start with the root directory. - catch (Exception ex) - { + catch (Exception ex) { System.out.println(ex.getMessage()); return false; } - } - else + } else return false; return true; @@ -383,32 +250,28 @@ else if (args[0].equals("mode")) *

  • /dir -> current_path/dir *
  • dir -> current_path/dir * + * * @param arg - String to be examined. * @return new_path * @throws Exception */ - private String determinePath(final String arg) throws Exception - { + private String determinePath(final String arg) throws Exception { String new_path = current_path; if (arg.equals(".")) // Current directory. { return new_path; - } - else if (arg.equals("..")) // Parent directory. + } else if (arg.equals("..")) // Parent directory. { AlarmTreeItem parent = model.findNode(current_path).getParent(); if (null != parent) new_path = parent.getPathName(); - } - else if (arg.startsWith(model.getRoot().getPathName())) // If starts from root, treat it as a whole path. + } else if (arg.startsWith(model.getRoot().getPathName())) // If starts from root, treat it as a whole path. { new_path = arg; - } - else if (arg.startsWith("/")) // Allow for "command /dir". + } else if (arg.startsWith("/")) // Allow for "command /dir". { new_path = current_path + arg; - } - else // Allow for "command dir". + } else // Allow for "command dir". { new_path = current_path + "/" + arg; } @@ -419,62 +282,57 @@ else if (arg.startsWith("/")) // Allow for "command /dir". return new_path; } - /** Handle commands - * - *
      - *
    • acknowledge /some/path - - * Acknowledge alarms in subtree - *
    • unacknowledge /some/path - - * Un-Acknowledge alarms in subtree - *
    • mode [normal|maintenance] - - * Select normal or maintenance mode - *
    • dump - - * Dumps complete alarm tree - *
    • dump /some/path - - * Dumps subtree - *
    • pvs - - * Prints all PVs - *
    • pvs /some/path - - * Prints PVs in subtree - *
    • pv name_of_PV - - * Prints that PV - *
    • disconnected - - * Prints all disconnected PVs - *
    • restart - - * Re-load configuration - *
    • shutdown - - * Quit - *
    + /** + * Handle commands * - * @param path Alarm tree path - * @param json Command + *
      + *
    • acknowledge /some/path - + * Acknowledge alarms in subtree + *
    • unacknowledge /some/path - + * Un-Acknowledge alarms in subtree + *
    • mode [normal|maintenance] - + * Select normal or maintenance mode + *
    • dump - + * Dumps complete alarm tree + *
    • dump /some/path - + * Dumps subtree + *
    • pvs - + * Prints all PVs + *
    • pvs /some/path - + * Prints PVs in subtree + *
    • pv name_of_PV - + * Prints that PV + *
    • disconnected - + * Prints all disconnected PVs + *
    • restart - + * Re-load configuration + *
    • shutdown - + * Quit + *
    + *

    + * @param path Alarm tree path + * @param json Command */ @Override - public void handleCommand(final String path, final String json) - { - try - { + public void handleCommand(final String path, final String json) { + try { final JsonNode jsonNode = (JsonNode) JsonModelReader.parseJsonText(json); final JsonNode commandNode = jsonNode.get(JsonTags.COMMAND); if (null == commandNode) throw new Exception("Command parsing failed."); final String command = commandNode.asText(); - if (command.startsWith("ack")) - { + if (command.startsWith("ack")) { final AlarmTreeItem node = model.findNode(path); if (node == null) throw new Exception("Unknown alarm tree node '" + path + "'"); acknowledge(node, true); - } - else if (command.startsWith("unack")) - { + } else if (command.startsWith("unack")) { final AlarmTreeItem node = model.findNode(path); if (node == null) throw new Exception("Unknown alarm tree node '" + path + "'"); acknowledge(node, false); - } - else if (JsonTags.MAINTENANCE.equals(command)) + } else if (JsonTags.MAINTENANCE.equals(command)) setMaintenanceMode(true); else if (JsonTags.NORMAL.equals(command)) setMaintenanceMode(false); @@ -482,60 +340,45 @@ else if (JsonTags.DISABLE_NOTIFY.equals(command)) setDisableNotify(true); else if (JsonTags.ENABLE_NOTIFY.equals(command)) setDisableNotify(false); - else if (command.equalsIgnoreCase("dump")) - { + else if (command.equalsIgnoreCase("dump")) { final AlarmTreeItem node; node = model.findNode(path); if (node == null) throw new Exception("Unknown alarm tree node '" + path + "'"); System.out.println(node.getPathName() + ":"); ModelPrinter.print(node); - } - else if (command.equalsIgnoreCase("pvs")) - { + } else if (command.equalsIgnoreCase("pvs")) { final AlarmTreeItem node; node = model.findNode(path); if (node == null) throw new Exception("Unknown alarm tree node '" + path + "'"); System.out.println("PVs for " + node.getPathName() + ":"); listPVs(node, PVMode.All); - } - else if (command.equalsIgnoreCase("disconnected")) - { + } else if (command.equalsIgnoreCase("disconnected")) { final AlarmTreeItem node; node = model.findNode(path); if (node == null) throw new Exception("Unknown alarm tree node '" + path + "'"); System.out.println("PVs for " + node.getPathName() + ":"); listPVs(node, PVMode.Disconnected); - } - else if (command.equalsIgnoreCase("pv")) - { + } else if (command.equalsIgnoreCase("pv")) { final AlarmServerPV pv = model.findPV(path); if (pv == null) throw new Exception("Unknown PV '" + path + "'"); listPVs(pv, PVMode.All); - } - else if (command.equals("shutdown")) - { + } else if (command.equals("shutdown")) { restart.offer(false); - } - else if (command.equalsIgnoreCase("restart")) - { + } else if (command.equalsIgnoreCase("restart")) { logger.log(Level.INFO, "Restart requested"); restart.offer(true); - } - else + } else throw new Exception("Unknown command."); - } - catch (Exception ex) - { + } catch (Exception ex) { logger.log(Level.WARNING, "Error for command. path: '" + path + "', JSON: '" + json + "'", ex); } } - private void setDisableNotify(final boolean disable_notify) - { + private void setDisableNotify(final boolean disable_notify) { // Any change? if (disable_notify == AlarmLogic.getDisableNotify()) return; @@ -546,8 +389,7 @@ private void setDisableNotify(final boolean disable_notify) model.sendStateUpdate(model.getRoot().getPathName(), model.getRoot().getState()); } - private void setMaintenanceMode(final boolean maintenance_mode) - { + private void setMaintenanceMode(final boolean maintenance_mode) { // Any change? if (maintenance_mode == AlarmLogic.getMaintenanceMode()) return; @@ -561,82 +403,73 @@ private void setMaintenanceMode(final boolean maintenance_mode) model.sendStateUpdate(model.getRoot().getPathName(), model.getRoot().getState()); } - /** @param node Node where to start ack'ing all INVALID or UNDEFINED alarms */ - private void acknowledgeInvalidUndefined(final AlarmTreeItem node) - { - if (node instanceof AlarmServerPV) - { + /** + * @param node Node where to start ack'ing all INVALID or UNDEFINED alarms + */ + private void acknowledgeInvalidUndefined(final AlarmTreeItem node) { + if (node instanceof AlarmServerPV) { final AlarmServerPV pv_node = (AlarmServerPV) node; if (pv_node.getState().severity.ordinal() >= SeverityLevel.INVALID.ordinal()) pv_node.acknowledge(true); - } - else + } else for (final AlarmTreeItem child : node.getChildren()) acknowledgeInvalidUndefined(child); } - private void acknowledge(final AlarmTreeItem node, final boolean acknowledge) - { - if (node instanceof AlarmServerPV) - { + private void acknowledge(final AlarmTreeItem node, final boolean acknowledge) { + if (node instanceof AlarmServerPV) { final AlarmServerPV pv_node = (AlarmServerPV) node; pv_node.acknowledge(acknowledge); - } - else + } else for (final AlarmTreeItem child : node.getChildren()) acknowledge(child, acknowledge); } - enum PVMode - { + enum PVMode { All, InActiveAlarm, InAlarm, Disconnected, Disabled - }; + } - private void listPVs(final AlarmTreeItem node, final PVMode which) - { + ; + + private void listPVs(final AlarmTreeItem node, final PVMode which) { listPVs(new AtomicInteger(), node, which); } - private void listPVs(final AtomicInteger count, final AlarmTreeItem node, final PVMode which) - { - if (node instanceof AlarmServerPV) - { + private void listPVs(final AtomicInteger count, final AlarmTreeItem node, final PVMode which) { + if (node instanceof AlarmServerPV) { final AlarmServerPV pv_node = (AlarmServerPV) node; - switch (which) - { - case Disabled: - if (pv_node.isEnabled()) - return; - break; - case Disconnected: - if (!pv_node.isEnabled() || pv_node.isConnected()) - return; - break; - case InActiveAlarm: - if (!pv_node.isEnabled() || !pv_node.getState().severity.isActive()) - return; - break; - case InAlarm: - if (!pv_node.isEnabled() || pv_node.getState().severity == SeverityLevel.OK) - return; - break; - default: - break; + switch (which) { + case Disabled: + if (pv_node.isEnabled()) + return; + break; + case Disconnected: + if (!pv_node.isEnabled() || pv_node.isConnected()) + return; + break; + case InActiveAlarm: + if (!pv_node.isEnabled() || !pv_node.getState().severity.isActive()) + return; + break; + case InAlarm: + if (!pv_node.isEnabled() || pv_node.getState().severity == SeverityLevel.OK) + return; + break; + default: + break; } System.out.format("%3d : ", count.incrementAndGet()); System.out.println(pv_node); - } - else + } else for (final AlarmTreeItem child : node.getChildren()) listPVs(count, child, which); } - private static void help() - { + private static void help() { // http://patorjk.com/software/taag/#p=display&f=Epic&t=Alarm%20Server System.out.println(" _______ _ _______ _______ _______ _______ _______ _______ _______ _______"); System.out.println("( ___ )( \\ ( ___ )( ____ )( ) ( ____ \\( ____ \\( ____ )|\\ /|( ____ \\( ____ )"); @@ -666,8 +499,7 @@ private static void help() } - public static void main(final String[] original_args) throws Exception - { + public static void main(final String[] original_args) throws Exception { LogManager.getLogManager().readConfiguration(AlarmServerMain.class.getResourceAsStream("/alarm_server_logging.properties")); String server = "localhost:9092"; @@ -679,91 +511,82 @@ public static void main(final String[] original_args) throws Exception final List args = new ArrayList<>(List.of(original_args)); final Iterator iter = args.iterator(); HashMap parsed_args = new HashMap(); - try - { + try { // define command line arguments - String help_arg = "-help"; - String help_alt_arg = "-h"; - String server_arg = "-server"; - String config_arg = "-config"; - String create_topics_arg = "-create_topics"; - String settings_arg = "-settings"; - String noshell_arg = "-noshell"; - String export_arg = "-export"; - String import_arg = "-import"; - String logging_arg = "-logging"; - String connect_secs_arg = "-connect_secs"; - String stable_secs_arg = "-stable_secs"; - String kafka_props_arg = "-kafka_properties"; + String help_arg = "-help"; + String help_alt_arg = "-h"; + String server_arg = "-server"; + String config_arg = "-config"; + String create_topics_arg = "-create_topics"; + String settings_arg = "-settings"; + String noshell_arg = "-noshell"; + String export_arg = "-export"; + String import_arg = "-import"; + String logging_arg = "-logging"; + String connect_secs_arg = "-connect_secs"; + String stable_secs_arg = "-stable_secs"; + String kafka_props_arg = "-kafka_properties"; Set options = Set.of( - server_arg, - config_arg, - settings_arg, - export_arg, - import_arg, - logging_arg, - connect_secs_arg, - stable_secs_arg, - kafka_props_arg); + server_arg, + config_arg, + settings_arg, + export_arg, + import_arg, + logging_arg, + connect_secs_arg, + stable_secs_arg, + kafka_props_arg); Set flags = Set.of( - help_arg, - help_alt_arg, - noshell_arg, - create_topics_arg + help_arg, + help_alt_arg, + noshell_arg, + create_topics_arg ); // to handle arguments that may be provided via a settings file // as well as directly on the commandline, map their relationship Map args_to_prefs = Map.ofEntries( - Map.entry(config_arg, "config_names"), - Map.entry(server_arg, "server"), - Map.entry(kafka_props_arg, "kafka_properties") + Map.entry(config_arg, "config_names"), + Map.entry(server_arg, "server"), + Map.entry(kafka_props_arg, "kafka_properties") ); - while (iter.hasNext()) - { + while (iter.hasNext()) { final String cmd = iter.next(); - if (options.contains(cmd)) - { - if (! iter.hasNext()) - throw new Exception("Missing argument for " + cmd); + if (options.contains(cmd)) { + if (!iter.hasNext()) + throw new Exception("Missing argument for " + cmd); final String arg = iter.next(); parsed_args.put(cmd, arg); - } - else if (flags.contains(cmd)) + } else if (flags.contains(cmd)) parsed_args.put(cmd, ""); else throw new Exception("Unknown option " + cmd); } - if (parsed_args.containsKey(help_arg) || parsed_args.containsKey(help_alt_arg)) - { + if (parsed_args.containsKey(help_arg) || parsed_args.containsKey(help_alt_arg)) { help(); return; } if (parsed_args.containsKey(logging_arg)) LogManager.getLogManager().readConfiguration(new FileInputStream(parsed_args.get(logging_arg))); - if (parsed_args.containsKey(settings_arg)) - { + if (parsed_args.containsKey(settings_arg)) { final String filename = parsed_args.get(settings_arg); logger.info("Loading settings from " + filename); PropertyPreferenceLoader.load(new FileInputStream(filename)); - final Preferences userPrefs = Preferences.userRoot().node("org/phoebus/applications/alarm"); + final Preferences userPrefs = Preferences.userRoot().node("org/phoebus/applications/alarm"); - for (Map.Entry entry: args_to_prefs.entrySet()) - { + for (Map.Entry entry : args_to_prefs.entrySet()) { final String prefKey = entry.getValue(); final String arg = entry.getKey(); - - if (parsed_args.containsKey(arg)) - { - logger.log(Level.WARNING,"Potentially conflicting setting: -settings/"+prefKey+": " + userPrefs.get(prefKey, "") + " and " + arg + ":" + parsed_args.get(arg)); - logger.log(Level.WARNING,"Using argument " + arg + " instead of -settings"); - logger.log(Level.WARNING,prefKey + ": " + parsed_args.get(arg)); - } - else if (Set.of(userPrefs.keys()).contains(prefKey)) + + if (parsed_args.containsKey(arg)) { + logger.log(Level.WARNING, "Potentially conflicting setting: -settings/" + prefKey + ": " + userPrefs.get(prefKey, "") + " and " + arg + ":" + parsed_args.get(arg)); + logger.log(Level.WARNING, "Using argument " + arg + " instead of -settings"); + logger.log(Level.WARNING, prefKey + ": " + parsed_args.get(arg)); + } else if (Set.of(userPrefs.keys()).contains(prefKey)) parsed_args.put(arg, userPrefs.get(prefKey, "")); } } @@ -772,40 +595,35 @@ else if (Set.of(userPrefs.keys()).contains(prefKey)) server = parsed_args.getOrDefault(server_arg, server); kafka_properties = parsed_args.getOrDefault(kafka_props_arg, kafka_properties); use_shell = !parsed_args.containsKey(noshell_arg); - + if (parsed_args.containsKey(connect_secs_arg)) AlarmStateInitializer.CONNECTION_SECS = AlarmConfigTool.CONNECTION_SECS - = Long.parseLong(parsed_args.get(connect_secs_arg)); + = Long.parseLong(parsed_args.get(connect_secs_arg)); if (parsed_args.containsKey(stable_secs_arg)) AlarmStateInitializer.STABILIZATION_SECS = AlarmConfigTool.STABILIZATION_SECS - = Long.parseLong(parsed_args.get(stable_secs_arg)); + = Long.parseLong(parsed_args.get(stable_secs_arg)); - if (parsed_args.containsKey(create_topics_arg)) - { + if (parsed_args.containsKey(create_topics_arg)) { logger.info("Discovering and creating any missing topics at " + server); CreateTopics.discoverAndCreateTopics(server, true, List.of(config, - config + AlarmSystemConstants.COMMAND_TOPIC_SUFFIX, - config + AlarmSystemConstants.TALK_TOPIC_SUFFIX), - kafka_properties); + config + AlarmSystemConstants.COMMAND_TOPIC_SUFFIX, + config + AlarmSystemConstants.TALK_TOPIC_SUFFIX), + kafka_properties); } - if (parsed_args.containsKey(export_arg)) - { + if (parsed_args.containsKey(export_arg)) { final String filename = parsed_args.get(export_arg); logger.info("Exporting model to " + filename); new AlarmConfigTool().exportModel(filename, server, config, kafka_properties); } - if (parsed_args.containsKey(import_arg)) - { + if (parsed_args.containsKey(import_arg)) { final String filename = parsed_args.get(import_arg); logger.info("Import model from " + filename); new AlarmConfigTool().importModel(filename, server, config, kafka_properties); } if (parsed_args.containsKey(export_arg) || parsed_args.containsKey(import_arg)) return; - } - catch (final Exception ex) - { + } catch (final Exception ex) { help(); System.out.println(); ex.printStackTrace(); diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java new file mode 100644 index 0000000000..36af1ca6a6 --- /dev/null +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.alarm.server; + +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.AdminClientConfig; +import org.apache.kafka.clients.admin.AlterConfigOp; +import org.apache.kafka.clients.admin.ConfigEntry; +import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.common.config.ConfigResource; +import org.phoebus.applications.alarm.client.KafkaHelper; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * Provides a utility to create the Kafka topics if needed, and then + * configure them based on preferences. + */ +public class TopicUtils { + + private static final Logger logger = Logger.getLogger(TopicUtils.class.getName()); + + /** + * Ensure that the required Kafka topics exist and are correctly configured. + *

    + * Creates and configures the main alarm topic (compacted) and command/talk topics (deleted). + * For more details on alarm topic configuration, see: + * Refer to Configure Alarm Topics + * + * @param server Kafka server + * @param topic Base topic name + * @param kafkaPropsFile Extra Kafka properties file + * @throws Exception If for instance an admin client could not be created or + * if the request to Kafka times out. + */ + public static void ensureKafkaTopics(String server, String topic, String kafkaPropsFile) throws Exception { + var kafkaProps = KafkaHelper.loadPropsFromFile(kafkaPropsFile); + kafkaProps.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, server); + try (AdminClient admin = AdminClient.create(kafkaProps)) { + Set topics = admin.listTopics().names().get(60, TimeUnit.SECONDS); + // Compacted topic + if (!topics.contains(topic)) { + createTopic(admin, topic); + } + setCompactedConfig(admin, topic); + + // Deleted topics + for (String suffix : List.of("Command", "Talk")) { + String deletedTopic = topic + suffix; + if (!topics.contains(deletedTopic)) { + createTopic(admin, deletedTopic); + } + setDeletedConfig(admin, deletedTopic); + } + } + } + + /** + * Create topics + * + * @param admin Admin client + * @param topic Topic name + * @throws Exception If topic could not be created + */ + private static void createTopic(AdminClient admin, String topic) throws Exception { + NewTopic newTopic = new NewTopic(topic, 1, (short) 1); + try { + admin.createTopics(List.of(newTopic)).all().get(); + logger.info("Created topic: " + topic); + } catch (Exception e) { + if (e.getCause() instanceof org.apache.kafka.common.errors.TopicExistsException) { + logger.info("Topic already exists: " + topic); + } else { + throw e; + } + } + } + + /** + * Configure topic for alarm state storage with compaction to retain latest state. + * For configuration information, see: + *

    + * Refer to Configure Alarm Topics + * + * @param admin Admin client + * @param topic Topic name + * @throws Exception If topic could not be configured + */ + private static void setCompactedConfig(AdminClient admin, String topic) throws Exception { + ConfigResource resource = new ConfigResource(ConfigResource.Type.TOPIC, topic); + List configOps = List.of( + new AlterConfigOp(new ConfigEntry("cleanup.policy", "compact"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("segment.ms", "10000"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("min.cleanable.dirty.ratio", "0.01"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("min.compaction.lag.ms", "1000"), AlterConfigOp.OpType.SET) + ); + admin.incrementalAlterConfigs(Map.of(resource, configOps)).all().get(); + logger.info("Set compacted config for topic: " + topic); + } + + /** + * Configure topic for command/talk messages with time-based deletion. + * For configuration information, see: + *

    + * Refer to Configure Alarm Topics + * + * @param admin Admin client + * @param topic Topic name + * @throws Exception If topic could not be configured + */ + private static void setDeletedConfig(AdminClient admin, String topic) throws Exception { + ConfigResource resource = new ConfigResource(ConfigResource.Type.TOPIC, topic); + List configOps = List.of( + new AlterConfigOp(new ConfigEntry("cleanup.policy", "delete"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("segment.ms", "10000"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("min.cleanable.dirty.ratio", "0.01"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("min.compaction.lag.ms", "1000"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("retention.ms", "20000"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("delete.retention.ms", "1000"), AlterConfigOp.OpType.SET), + new AlterConfigOp(new ConfigEntry("file.delete.delay.ms", "1000"), AlterConfigOp.OpType.SET) + ); + admin.incrementalAlterConfigs(Map.of(resource, configOps)).all().get(); + logger.info("Set deleted config for topic: " + topic); + } +} From f065a49bf165c9b84f7bcb06049455c6850e9352 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Wed, 25 Feb 2026 16:44:16 +0000 Subject: [PATCH 2/4] Removed code related to create_topics command line option --- .../alarm/server/AlarmServerMain.java | 38 ++++++------------- .../applications/alarm/server/TopicUtils.java | 6 +-- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java index 1a5b750800..0de566582a 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/AlarmServerMain.java @@ -8,7 +8,6 @@ package org.phoebus.applications.alarm.server; import com.fasterxml.jackson.databind.JsonNode; -import org.phoebus.applications.alarm.AlarmSystemConstants; import org.phoebus.applications.alarm.client.ClientState; import org.phoebus.applications.alarm.model.AlarmTreeItem; import org.phoebus.applications.alarm.model.AlarmTreeLeaf; @@ -310,6 +309,7 @@ private String determinePath(final String arg) throws Exception { * Quit * *

    + * * @param path Alarm tree path * @param json Command */ @@ -407,8 +407,7 @@ private void setMaintenanceMode(final boolean maintenance_mode) { * @param node Node where to start ack'ing all INVALID or UNDEFINED alarms */ private void acknowledgeInvalidUndefined(final AlarmTreeItem node) { - if (node instanceof AlarmServerPV) { - final AlarmServerPV pv_node = (AlarmServerPV) node; + if (node instanceof AlarmServerPV pv_node) { if (pv_node.getState().severity.ordinal() >= SeverityLevel.INVALID.ordinal()) pv_node.acknowledge(true); } else @@ -417,8 +416,7 @@ private void acknowledgeInvalidUndefined(final AlarmTreeItem node) { } private void acknowledge(final AlarmTreeItem node, final boolean acknowledge) { - if (node instanceof AlarmServerPV) { - final AlarmServerPV pv_node = (AlarmServerPV) node; + if (node instanceof AlarmServerPV pv_node) { pv_node.acknowledge(acknowledge); } else for (final AlarmTreeItem child : node.getChildren()) @@ -433,15 +431,12 @@ enum PVMode { Disabled } - ; - private void listPVs(final AlarmTreeItem node, final PVMode which) { listPVs(new AtomicInteger(), node, which); } private void listPVs(final AtomicInteger count, final AlarmTreeItem node, final PVMode which) { - if (node instanceof AlarmServerPV) { - final AlarmServerPV pv_node = (AlarmServerPV) node; + if (node instanceof AlarmServerPV pv_node) { switch (which) { case Disabled: if (pv_node.isEnabled()) @@ -485,8 +480,6 @@ private static void help() { System.out.println("-help - This text"); System.out.println("-server localhost:9092 - Kafka server with port number"); System.out.println("-config Accelerator - Alarm configuration"); - // Don't mention this option, prefer examples/create_topics.sh - // System.out.println("-create_topics - Create Kafka topics for alarm configuration?"); System.out.println("-settings settings.{xml,ini} - Import preferences (PV connectivity) from property format file"); System.out.println("-noshell - Disable the command shell for running without a terminal"); System.out.println("-export config.xml - Export alarm configuration to file"); @@ -517,7 +510,6 @@ public static void main(final String[] original_args) throws Exception { String help_alt_arg = "-h"; String server_arg = "-server"; String config_arg = "-config"; - String create_topics_arg = "-create_topics"; String settings_arg = "-settings"; String noshell_arg = "-noshell"; String export_arg = "-export"; @@ -541,8 +533,7 @@ public static void main(final String[] original_args) throws Exception { Set flags = Set.of( help_arg, help_alt_arg, - noshell_arg, - create_topics_arg + noshell_arg ); // to handle arguments that may be provided via a settings file @@ -570,8 +561,9 @@ public static void main(final String[] original_args) throws Exception { help(); return; } - if (parsed_args.containsKey(logging_arg)) + if (parsed_args.containsKey(logging_arg)) { LogManager.getLogManager().readConfiguration(new FileInputStream(parsed_args.get(logging_arg))); + } if (parsed_args.containsKey(settings_arg)) { final String filename = parsed_args.get(settings_arg); logger.info("Loading settings from " + filename); @@ -596,20 +588,13 @@ public static void main(final String[] original_args) throws Exception { kafka_properties = parsed_args.getOrDefault(kafka_props_arg, kafka_properties); use_shell = !parsed_args.containsKey(noshell_arg); - if (parsed_args.containsKey(connect_secs_arg)) + if (parsed_args.containsKey(connect_secs_arg)) { AlarmStateInitializer.CONNECTION_SECS = AlarmConfigTool.CONNECTION_SECS = Long.parseLong(parsed_args.get(connect_secs_arg)); - - if (parsed_args.containsKey(stable_secs_arg)) + } + if (parsed_args.containsKey(stable_secs_arg)) { AlarmStateInitializer.STABILIZATION_SECS = AlarmConfigTool.STABILIZATION_SECS = Long.parseLong(parsed_args.get(stable_secs_arg)); - - if (parsed_args.containsKey(create_topics_arg)) { - logger.info("Discovering and creating any missing topics at " + server); - CreateTopics.discoverAndCreateTopics(server, true, List.of(config, - config + AlarmSystemConstants.COMMAND_TOPIC_SUFFIX, - config + AlarmSystemConstants.TALK_TOPIC_SUFFIX), - kafka_properties); } if (parsed_args.containsKey(export_arg)) { final String filename = parsed_args.get(export_arg); @@ -621,8 +606,9 @@ public static void main(final String[] original_args) throws Exception { logger.info("Import model from " + filename); new AlarmConfigTool().importModel(filename, server, config, kafka_properties); } - if (parsed_args.containsKey(export_arg) || parsed_args.containsKey(import_arg)) + if (parsed_args.containsKey(export_arg) || parsed_args.containsKey(import_arg)) { return; + } } catch (final Exception ex) { help(); System.out.println(); diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java index 36af1ca6a6..c279de7957 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java @@ -33,11 +33,11 @@ public class TopicUtils { * For more details on alarm topic configuration, see: * Refer to Configure Alarm Topics * - * @param server Kafka server - * @param topic Base topic name + * @param server Kafka server + * @param topic Base topic name * @param kafkaPropsFile Extra Kafka properties file * @throws Exception If for instance an admin client could not be created or - * if the request to Kafka times out. + * if the request to Kafka times out. */ public static void ensureKafkaTopics(String server, String topic, String kafkaPropsFile) throws Exception { var kafkaProps = KafkaHelper.loadPropsFromFile(kafkaPropsFile); From e690a58647b29a2d65a07fabae0d8457de985812 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 26 Feb 2026 15:25:14 +0000 Subject: [PATCH 3/4] Support for customization of number of partitions and replication factor --- docs/pixi.lock | 229 +++++++++++++++++- .../main/resources/alarm_server.properties | 5 + 2 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 services/alarm-server/src/main/resources/alarm_server.properties diff --git a/docs/pixi.lock b/docs/pixi.lock index 432be73fb7..e5b80d96dc 100644 --- a/docs/pixi.lock +++ b/docs/pixi.lock @@ -34,32 +34,43 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c6/fd64ffd75d47c4fcf6c65808cc5c5c75e5d4357c197d3741ee1339e91257/picobox-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/3d/6b41fe1637cd53c4b10d56e0e6f396546f837973dabf9c4b2a1de44620ac/sphinx_mdinclude-0.6.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/3a/7c5ec79cee2d9505ef9079fc55cdc866e38aa68241982bf103604013f5c0/sphinxcontrib_openapi-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl @@ -82,32 +93,43 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h8210216_2.conda - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c6/fd64ffd75d47c4fcf6c65808cc5c5c75e5d4357c197d3741ee1339e91257/picobox-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/3d/6b41fe1637cd53c4b10d56e0e6f396546f837973dabf9c4b2a1de44620ac/sphinx_mdinclude-0.6.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/3a/7c5ec79cee2d9505ef9079fc55cdc866e38aa68241982bf103604013f5c0/sphinxcontrib_openapi-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl @@ -131,32 +153,43 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c6/fd64ffd75d47c4fcf6c65808cc5c5c75e5d4357c197d3741ee1339e91257/picobox-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/3d/6b41fe1637cd53c4b10d56e0e6f396546f837973dabf9c4b2a1de44620ac/sphinx_mdinclude-0.6.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/3a/7c5ec79cee2d9505ef9079fc55cdc866e38aa68241982bf103604013f5c0/sphinxcontrib_openapi-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl @@ -181,33 +214,44 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_32.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - pypi: https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c6/fd64ffd75d47c4fcf6c65808cc5c5c75e5d4357c197d3741ee1339e91257/picobox-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/3d/6b41fe1637cd53c4b10d56e0e6f396546f837973dabf9c4b2a1de44620ac/sphinx_mdinclude-0.6.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/3a/7c5ec79cee2d9505ef9079fc55cdc866e38aa68241982bf103604013f5c0/sphinxcontrib_openapi-0.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl @@ -288,14 +332,25 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/websockets-15.0.1-py314h31f8a6b_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py314h31f8a6b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c6/fd64ffd75d47c4fcf6c65808cc5c5c75e5d4357c197d3741ee1339e91257/picobox-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/3d/6b41fe1637cd53c4b10d56e0e6f396546f837973dabf9c4b2a1de44620ac/sphinx_mdinclude-0.6.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/3a/7c5ec79cee2d9505ef9079fc55cdc866e38aa68241982bf103604013f5c0/sphinxcontrib_openapi-0.9.0-py3-none-any.whl - pypi: ./ osx-64: - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda @@ -358,14 +413,25 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/websockets-15.0.1-py314hcfd16f8_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstandard-0.25.0-py314h12c88b1_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/zstd-1.5.7-h8210216_2.conda + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c6/fd64ffd75d47c4fcf6c65808cc5c5c75e5d4357c197d3741ee1339e91257/picobox-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/3d/6b41fe1637cd53c4b10d56e0e6f396546f837973dabf9c4b2a1de44620ac/sphinx_mdinclude-0.6.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/3a/7c5ec79cee2d9505ef9079fc55cdc866e38aa68241982bf103604013f5c0/sphinxcontrib_openapi-0.9.0-py3-none-any.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda @@ -429,14 +495,25 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/websockets-15.0.1-py314hf17b0b1_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py314h163e31d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c6/fd64ffd75d47c4fcf6c65808cc5c5c75e5d4357c197d3741ee1339e91257/picobox-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/3d/6b41fe1637cd53c4b10d56e0e6f396546f837973dabf9c4b2a1de44620ac/sphinx_mdinclude-0.6.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/3a/7c5ec79cee2d9505ef9079fc55cdc866e38aa68241982bf103604013f5c0/sphinxcontrib_openapi-0.9.0-py3-none-any.whl - pypi: ./ win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/alabaster-1.0.0-pyhd8ed1ab_1.conda @@ -501,14 +578,25 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py314h4667ab5_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda + - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/c6/fd64ffd75d47c4fcf6c65808cc5c5c75e5d4357c197d3741ee1339e91257/picobox-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/3d/6b41fe1637cd53c4b10d56e0e6f396546f837973dabf9c4b2a1de44620ac/sphinx_mdinclude-0.6.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/3a/7c5ec79cee2d9505ef9079fc55cdc866e38aa68241982bf103604013f5c0/sphinxcontrib_openapi-0.9.0-py3-none-any.whl - pypi: ./ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -567,6 +655,11 @@ packages: - pkg:pypi/anyio?source=hash-mapping size: 138159 timestamp: 1758634638734 +- pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + name: attrs + version: 25.4.0 + sha256: adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl name: babel version: 2.17.0 @@ -867,6 +960,20 @@ packages: - pkg:pypi/colorama?source=hash-mapping size: 27011 timestamp: 1733218222191 +- pypi: https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl + name: deepmerge + version: '2.0' + sha256: 6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00 + requires_dist: + - typing-extensions ; python_full_version < '3.10' + - validate-pyproject[all] ; extra == 'dev' + - pyupgrade ; extra == 'dev' + - black ; extra == 'dev' + - mypy ; extra == 'dev' + - pytest ; extra == 'dev' + - build ; extra == 'dev' + - twine ; extra == 'dev' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl name: docutils version: 0.21.2 @@ -1020,6 +1127,40 @@ packages: - pkg:pypi/jinja2?source=hash-mapping size: 112714 timestamp: 1741263433881 +- pypi: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl + name: jsonschema + version: 4.26.0 + sha256: d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + requires_dist: + - attrs>=22.2.0 + - jsonschema-specifications>=2023.3.6 + - referencing>=0.28.4 + - rpds-py>=0.25.0 + - fqdn ; extra == 'format' + - idna ; extra == 'format' + - isoduration ; extra == 'format' + - jsonpointer>1.13 ; extra == 'format' + - rfc3339-validator ; extra == 'format' + - rfc3987 ; extra == 'format' + - uri-template ; extra == 'format' + - webcolors>=1.11 ; extra == 'format' + - fqdn ; extra == 'format-nongpl' + - idna ; extra == 'format-nongpl' + - isoduration ; extra == 'format-nongpl' + - jsonpointer>1.13 ; extra == 'format-nongpl' + - rfc3339-validator ; extra == 'format-nongpl' + - rfc3986-validator>0.1.0 ; extra == 'format-nongpl' + - rfc3987-syntax>=1.1.0 ; extra == 'format-nongpl' + - uri-template ; extra == 'format-nongpl' + - webcolors>=24.6.0 ; extra == 'format-nongpl' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl + name: jsonschema-specifications + version: 2025.9.1 + sha256: 98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe + requires_dist: + - referencing>=0.31.0 + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1aa0949_4.conda sha256: 96b6900ca0489d9e5d0318a6b49f8eff43fd85fef6e07cb0c25344ee94cd7a3a md5: c94ab6ff54ba5172cf1c58267005670f @@ -1485,6 +1626,13 @@ packages: version: 0.1.2 sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl + name: mistune + version: 3.2.0 + sha256: febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1 + requires_dist: + - typing-extensions ; python_full_version < '3.11' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl name: myst-parser version: 4.0.1 @@ -1617,13 +1765,20 @@ packages: - pypi: ./ name: phoebus-docs version: 0.1.0 - sha256: 7fcc5415d986f427a29b6c8400b05968b42c025ba3ae8860d3c11ad61939ce49 + sha256: 4449b96d2acdc1d55c929adba70383673acbb7e99959b395ac1ffc25fd7ed521 requires_dist: - sphinx>=8.2 - sphinx-rtd-theme>=3.0 - myst-parser>=4.0 - setuptools + - sphinxcontrib-openapi + requires_python: '>=3.9' editable: true +- pypi: https://files.pythonhosted.org/packages/2d/c6/fd64ffd75d47c4fcf6c65808cc5c5c75e5d4357c197d3741ee1339e91257/picobox-4.0.0-py3-none-any.whl + name: picobox + version: 4.0.0 + sha256: 4c27eb689fe45dabd9e64c382e04418147d0b746d155b4e80057dbb7ff82027e + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 md5: 12c566707c80111f9799308d9e265aef @@ -1855,6 +2010,15 @@ packages: purls: [] size: 252359 timestamp: 1740379663071 +- pypi: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl + name: referencing + version: 0.37.0 + sha256: 381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 + requires_dist: + - attrs>=22.2.0 + - rpds-py>=0.7.0 + - typing-extensions>=4.4.0 ; python_full_version < '3.13' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl name: requests version: 2.32.5 @@ -1904,6 +2068,26 @@ packages: - pkg:pypi/roman-numerals-py?source=hash-mapping size: 13348 timestamp: 1740240332327 +- pypi: https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl + name: rpds-py + version: 0.30.0 + sha256: ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl + name: rpds-py + version: 0.30.0 + sha256: 95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl + name: rpds-py + version: 0.30.0 + sha256: 68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: rpds-py + version: 0.30.0 + sha256: 47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl name: setuptools version: 80.9.0 @@ -2078,6 +2262,29 @@ packages: - pkg:pypi/sphinx-autobuild?source=hash-mapping size: 19892 timestamp: 1762270046787 +- pypi: https://files.pythonhosted.org/packages/42/3d/6b41fe1637cd53c4b10d56e0e6f396546f837973dabf9c4b2a1de44620ac/sphinx_mdinclude-0.6.2-py3-none-any.whl + name: sphinx-mdinclude + version: 0.6.2 + sha256: 648e78edb067c0e4bffc22943278d49d54a0714494743592032fa3ad82a86984 + requires_dist: + - mistune>=3.0,<4.0 + - docutils>=0.19,<1.0 + - pygments>=2.8 + - sphinx>=6 + - docutils==0.20.1 ; python_full_version < '3.9' and extra == 'dev' + - docutils==0.21.2 ; python_full_version >= '3.9' and extra == 'dev' + - mistune==3.0.2 ; extra == 'dev' + - attribution==1.7.1 ; extra == 'dev' + - black==24.4.2 ; extra == 'dev' + - coverage==7.5.1 ; extra == 'dev' + - flake8==7.0.0 ; extra == 'dev' + - flit==3.9.0 ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - sphinx==7.1.2 ; python_full_version < '3.9' and extra == 'dev' + - sphinx==7.3.7 ; python_full_version >= '3.9' and extra == 'dev' + - ufmt==2.5.1 ; extra == 'dev' + - usort==1.0.8.post1 ; extra == 'dev' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl name: sphinx-rtd-theme version: 3.0.2 @@ -2161,6 +2368,13 @@ packages: - pkg:pypi/sphinxcontrib-htmlhelp?source=hash-mapping size: 32895 timestamp: 1733754385092 +- pypi: https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl + name: sphinxcontrib-httpdomain + version: 2.0.0 + sha256: e968775c9994f8139cb6ff91e1f6a8557396a2cc08073997eed10d9b39f96df3 + requires_dist: + - sphinx>=6.0 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl name: sphinxcontrib-jquery version: '4.1' @@ -2188,6 +2402,19 @@ packages: - pkg:pypi/sphinxcontrib-jsmath?source=hash-mapping size: 10462 timestamp: 1733753857224 +- pypi: https://files.pythonhosted.org/packages/32/3a/7c5ec79cee2d9505ef9079fc55cdc866e38aa68241982bf103604013f5c0/sphinxcontrib_openapi-0.9.0-py3-none-any.whl + name: sphinxcontrib-openapi + version: 0.9.0 + sha256: 19037dc091e741b2f84e9c17ff201f70f2ac5c192c883e055bb2d96e9facfbdf + requires_dist: + - sphinx>=2.0 + - sphinxcontrib-httpdomain>=1.5.0 + - pyyaml>=3.12 + - jsonschema>=2.5.1 + - sphinx-mdinclude>=0.5.2 + - picobox>=2.2 + - deepmerge>=0.1 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl name: sphinxcontrib-qthelp version: 2.0.0 diff --git a/services/alarm-server/src/main/resources/alarm_server.properties b/services/alarm-server/src/main/resources/alarm_server.properties new file mode 100644 index 0000000000..234d218f98 --- /dev/null +++ b/services/alarm-server/src/main/resources/alarm_server.properties @@ -0,0 +1,5 @@ +# Replication factor +replicationFactor=1 + +# Partitions +numberOfPartitions=1 \ No newline at end of file From 00329fd5a4989144b6f7b72f971bb1fc5f87dca1 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 27 Feb 2026 10:07:15 +0000 Subject: [PATCH 4/4] Support default Kafka topic config to be overriden in settings --- .../applications/alarm/server/TopicUtils.java | 13 ++++++++++++- .../src/main/resources/alarm_server.properties | 8 ++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java index c279de7957..bc21c97b20 100644 --- a/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java +++ b/services/alarm-server/src/main/java/org/phoebus/applications/alarm/server/TopicUtils.java @@ -11,6 +11,8 @@ import org.apache.kafka.clients.admin.NewTopic; import org.apache.kafka.common.config.ConfigResource; import org.phoebus.applications.alarm.client.KafkaHelper; +import org.phoebus.framework.preferences.AnnotatedPreferences; +import org.phoebus.framework.preferences.Preference; import java.util.List; import java.util.Map; @@ -26,6 +28,15 @@ public class TopicUtils { private static final Logger logger = Logger.getLogger(TopicUtils.class.getName()); + @SuppressWarnings("unused") + @Preference private static int numberOfPartitions; + @SuppressWarnings("unused") + @Preference private static int replicationFactor; + + static { + AnnotatedPreferences.initialize(TopicUtils.class, "/alarm_server.properties"); + } + /** * Ensure that the required Kafka topics exist and are correctly configured. *

    @@ -69,7 +80,7 @@ public static void ensureKafkaTopics(String server, String topic, String kafkaPr * @throws Exception If topic could not be created */ private static void createTopic(AdminClient admin, String topic) throws Exception { - NewTopic newTopic = new NewTopic(topic, 1, (short) 1); + NewTopic newTopic = new NewTopic(topic, numberOfPartitions, (short) replicationFactor); try { admin.createTopics(List.of(newTopic)).all().get(); logger.info("Created topic: " + topic); diff --git a/services/alarm-server/src/main/resources/alarm_server.properties b/services/alarm-server/src/main/resources/alarm_server.properties index 234d218f98..66f14f9af0 100644 --- a/services/alarm-server/src/main/resources/alarm_server.properties +++ b/services/alarm-server/src/main/resources/alarm_server.properties @@ -1,5 +1,9 @@ -# Replication factor +############################################### +# Package org.phoebus.applications.alarm.server +############################################### + +# Kafka topic replication factor replicationFactor=1 -# Partitions +# Kafka partition count numberOfPartitions=1 \ No newline at end of file