From 3b22da57a641e34be81bd5264a3724f18d54f4e2 Mon Sep 17 00:00:00 2001 From: JohnnyQ5 Date: Tue, 24 Jan 2023 13:54:31 +0000 Subject: [PATCH 1/8] Set version to 1.3.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9b9884f..d34aa95 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 life.qbic postman-cli - 1.2.0 + 1.3.0 Postman cli http://github.com/qbicsoftware/postman-cli A client software written in Java for dataset downloads from QBiC's data management system openBIS From 4f066a9d1052b903375bd8ec21b2aa44b0f4f9ba Mon Sep 17 00:00:00 2001 From: Aline Breitinger <93044475+Aline-9@users.noreply.github.com> Date: Fri, 17 Feb 2023 13:20:22 +0100 Subject: [PATCH 2/8] Added version information option (#139) * Added version information option * Rename variables --------- Co-authored-by: Tobias Koch --- pom.xml | 1 + .../commandline/ManifestVersionProvider.java | 15 ++++++++++++ .../PostmanCommandLineOptions.java | 23 +++++++++++++------ 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 src/main/java/life/qbic/io/commandline/ManifestVersionProvider.java diff --git a/pom.xml b/pom.xml index d34aa95..e3a5987 100644 --- a/pom.xml +++ b/pom.xml @@ -127,6 +127,7 @@ life.qbic.App + true diff --git a/src/main/java/life/qbic/io/commandline/ManifestVersionProvider.java b/src/main/java/life/qbic/io/commandline/ManifestVersionProvider.java new file mode 100644 index 0000000..afa48a3 --- /dev/null +++ b/src/main/java/life/qbic/io/commandline/ManifestVersionProvider.java @@ -0,0 +1,15 @@ +package life.qbic.io.commandline; + +import picocli.CommandLine; + +public class ManifestVersionProvider implements CommandLine.IVersionProvider { + @Override + public String[] getVersion() { + String implementationVersion = getClass().getPackage().getImplementationVersion(); + return new String[]{ + "version: " + implementationVersion, + "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", + "OS: ${os.name} ${os.version} ${os.arch}" + }; + } +} diff --git a/src/main/java/life/qbic/io/commandline/PostmanCommandLineOptions.java b/src/main/java/life/qbic/io/commandline/PostmanCommandLineOptions.java index c37171d..0ec9d45 100644 --- a/src/main/java/life/qbic/io/commandline/PostmanCommandLineOptions.java +++ b/src/main/java/life/qbic/io/commandline/PostmanCommandLineOptions.java @@ -1,12 +1,5 @@ package life.qbic.io.commandline; -import static java.util.Objects.isNull; -import static java.util.Objects.nonNull; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; import life.qbic.App; import life.qbic.io.parser.IdentifierParser; import life.qbic.model.download.Authentication; @@ -20,8 +13,17 @@ import picocli.CommandLine.Option; import picocli.CommandLine.Parameters; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + // main command with format specifiers for the usage help message @Command(name = "postman-cli", + versionProvider = ManifestVersionProvider.class, footer = "Optional: specify a config file by running postman with '@/path/to/config.txt'. Details can be found in the README.", description = "A client software for dataset downloads from QBiC's data management system openBIS.", usageHelpAutoWidth = true, @@ -35,9 +37,16 @@ public class PostmanCommandLineOptions { private static final Logger LOG = LogManager.getLogger(QbicDataDownloader.class); + @Option(names = {"-V", "--version"}, + versionHelp = true, + description = "print version information", + scope = CommandLine.ScopeType.INHERIT) + boolean versionRequested; + //parameters to format the help message @Command(name = "download", + versionProvider = ManifestVersionProvider.class, description = "Download data from OpenBis", usageHelpAutoWidth = true, sortOptions = false, From 2977ca575d9707d1664c99d31fb7c710697b0ab2 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Fri, 17 Feb 2023 14:47:35 +0100 Subject: [PATCH 3/8] Require commons-codec version >= 1.13 (#149) --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index e3a5987..3d4d6f8 100644 --- a/pom.xml +++ b/pom.xml @@ -98,6 +98,13 @@ httpclient 4.5.13 + + + commons-codec + commons-codec + [1.13,) + + life.qbic From bae1ecec64e7ff3209c5d04a674578fc0294464f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Feb 2023 15:04:18 +0100 Subject: [PATCH 4/8] [DEPENDABOT]: Bump groovy from 2.5.1 to 3.0.9 (#85) Bumps [groovy](https://github.com/apache/groovy) from 2.5.1 to 3.0.9. - [Release notes](https://github.com/apache/groovy/releases) - [Commits](https://github.com/apache/groovy/commits) --- updated-dependencies: - dependency-name: org.codehaus.groovy:groovy:indy dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3d4d6f8..a4bba84 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ 1.8 1.8 UTF-8 - 3.0.8 + 3.0.15 2.17.1 From 467bf6308ee0d878fc486fc165b25efd56491296 Mon Sep 17 00:00:00 2001 From: Aline Breitinger <93044475+Aline-9@users.noreply.github.com> Date: Fri, 17 Feb 2023 15:18:36 +0100 Subject: [PATCH 5/8] Update JavaDoc (#138) * edited & added Javadocs * very small refactor * Update branch --------- Co-authored-by: Tobias Koch --- .../io/commandline/OpenBISPasswordParser.java | 3 +-- .../PostmanCommandLineOptions.java | 4 ++++ .../qbic/model/download/OutputPathFinder.java | 24 +++++++++++++++++++ .../qbic/model/download/QbicDataDisplay.java | 10 ++++++++ .../model/download/QbicDataDownloader.java | 2 +- 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/main/java/life/qbic/io/commandline/OpenBISPasswordParser.java b/src/main/java/life/qbic/io/commandline/OpenBISPasswordParser.java index cb5fb60..55923f7 100644 --- a/src/main/java/life/qbic/io/commandline/OpenBISPasswordParser.java +++ b/src/main/java/life/qbic/io/commandline/OpenBISPasswordParser.java @@ -23,8 +23,7 @@ public static String readPasswordFromConsole() { */ public static Optional readPasswordFromEnvVariable(String variableName) { - Optional password = Optional.ofNullable(System.getenv(variableName)); - return password; + return Optional.ofNullable(System.getenv(variableName)); } diff --git a/src/main/java/life/qbic/io/commandline/PostmanCommandLineOptions.java b/src/main/java/life/qbic/io/commandline/PostmanCommandLineOptions.java index 0ec9d45..16474c4 100644 --- a/src/main/java/life/qbic/io/commandline/PostmanCommandLineOptions.java +++ b/src/main/java/life/qbic/io/commandline/PostmanCommandLineOptions.java @@ -157,6 +157,10 @@ void listDatasets() scope = CommandLine.ScopeType.INHERIT) public boolean helpRequested = false; + /** + * @return sample identifiers + * @throws IOException if no ids or command line argument ids & file were provided + */ private List verifyProvidedIdentifiers() throws IOException { if ((isNull(ids) || ids.isEmpty()) && isNull(filePath)) { System.err.println( diff --git a/src/main/java/life/qbic/model/download/OutputPathFinder.java b/src/main/java/life/qbic/model/download/OutputPathFinder.java index 5b8a216..2dbb977 100644 --- a/src/main/java/life/qbic/model/download/OutputPathFinder.java +++ b/src/main/java/life/qbic/model/download/OutputPathFinder.java @@ -8,10 +8,18 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +/** + * Methods to determine the final path for the output directory. + * The requested data will be downloaded into this directory. + */ public class OutputPathFinder { private static final Logger LOG = LogManager.getLogger(OutputPathFinder.class); + /** + * @param path to be shortened + * @return path that has no parents (top directory) + */ private static Path getTopDirectory(Path path) { Path currentPath = Paths.get(path.toString()); Path parentPath; @@ -22,11 +30,20 @@ private static Path getTopDirectory(Path path) { return currentPath; } + /** + * @param possiblePath: string that could be an existing Path to a directory + * @return true if path exists, false otherwise + */ private static boolean isPathValid(String possiblePath){ Path path = Paths.get(possiblePath); return Files.isDirectory(path); } + /** + * @param file to download + * @param conservePaths if true, directory structure will be conserved + * @return final path to file itself + */ private static Path determineFinalPathFromDataset(DataSetFile file, Boolean conservePaths ) { Path finalPath; if (conservePaths) { @@ -40,6 +57,13 @@ private static Path determineFinalPathFromDataset(DataSetFile file, Boolean cons return finalPath; } + /** + * @param outputPath provided by user + * @param prefix sample code + * @param file to download + * @param conservePaths provided by user + * @return output directory path + */ public static Path determineOutputDirectory(String outputPath, Path prefix, DataSetFile file, boolean conservePaths){ Path filePath = determineFinalPathFromDataset(file, conservePaths); String path = File.separator + prefix.toString() + File.separator + filePath.toString(); diff --git a/src/main/java/life/qbic/model/download/QbicDataDisplay.java b/src/main/java/life/qbic/model/download/QbicDataDisplay.java index d7ab04e..7f12cdf 100644 --- a/src/main/java/life/qbic/model/download/QbicDataDisplay.java +++ b/src/main/java/life/qbic/model/download/QbicDataDisplay.java @@ -19,6 +19,9 @@ import life.qbic.model.files.FileSize; import life.qbic.model.files.FileSizeFormatter; +/** + * Lists information about requested datasets and their files + */ public class QbicDataDisplay { String sessionToken; @@ -31,6 +34,13 @@ public class QbicDataDisplay { private final QbicDataFinder qbicDataFinder; + /** + * Constructor for a QbicDataDisplay instance + * + * @param AppServerUri The openBIS application server URL (AS) + * @param DataServerUri The openBIS datastore server URL (DSS) + * @param sessionToken The session token for the datastore & application servers + */ public QbicDataDisplay( String AppServerUri, List dataServerUris, diff --git a/src/main/java/life/qbic/model/download/QbicDataDownloader.java b/src/main/java/life/qbic/model/download/QbicDataDownloader.java index 6a7401f..b335e55 100644 --- a/src/main/java/life/qbic/model/download/QbicDataDownloader.java +++ b/src/main/java/life/qbic/model/download/QbicDataDownloader.java @@ -46,7 +46,7 @@ public class QbicDataDownloader { private final QbicDataFinder qbicDataFinder; /** - * Constructor for a QBiCDataLoaderInstance + * Constructor for a QBiCDataDownloader instance * * @param AppServerUri The openBIS application server URL (AS) * @param dataServerUris The openBIS datastore server URLs (DSS) From a91043e9e2b62e6a3d4dc4a901efb85777dc46fe Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Fri, 17 Feb 2023 15:53:38 +0100 Subject: [PATCH 6/8] Fix log folder not created reliably (#151) --- pom.xml | 4 ++-- src/main/java/life/qbic/App.java | 19 ++++++++++++++----- .../life/qbic/io/parser/IdentifierParser.java | 2 +- .../java/life/qbic/model/Configuration.java | 4 ++-- .../qbic/model/download/Authentication.java | 2 +- .../qbic/model/download/QbicDataDisplay.java | 17 +++++++---------- 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/pom.xml b/pom.xml index a4bba84..6b3eb0b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,13 +1,13 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 life.qbic postman-cli 1.3.0 Postman cli - http://github.com/qbicsoftware/postman-cli + https://github.com/qbicsoftware/postman-cli A client software written in Java for dataset downloads from QBiC's data management system openBIS jar diff --git a/src/main/java/life/qbic/App.java b/src/main/java/life/qbic/App.java index 4f216a5..5a7d155 100644 --- a/src/main/java/life/qbic/App.java +++ b/src/main/java/life/qbic/App.java @@ -1,10 +1,8 @@ package life.qbic; -import java.io.File; -import java.io.IOException; -import java.util.Optional; import life.qbic.io.commandline.OpenBISPasswordParser; import life.qbic.io.commandline.PostmanCommandLineOptions; +import life.qbic.model.Configuration; import life.qbic.model.download.Authentication; import life.qbic.model.download.AuthenticationException; import life.qbic.model.download.ConnectionException; @@ -12,6 +10,9 @@ import org.apache.logging.log4j.Logger; import picocli.CommandLine; +import java.io.File; +import java.util.Optional; + /** * postman for staging data from openBIS */ @@ -19,7 +20,7 @@ public class App { private static final Logger LOG = LogManager.getLogger(App.class); - public static void main(String[] args) throws IOException { + public static void main(String[] args) { CommandLine cmd = new CommandLine(new PostmanCommandLineOptions()); int exitCode = cmd.execute(args); @@ -67,7 +68,15 @@ public static Authentication loginToOpenBIS( } // Ensure 'logs' folder is created - new File(System.getProperty("user.dir") + File.separator + "logs").mkdirs(); + File logFolder = new File(Configuration.LOG_PATH.toAbsolutePath().toString()); + if (!logFolder.exists()) { + boolean logFolderCreated = logFolder.mkdirs(); + if (!logFolderCreated) { + LOG.error("Could not create log folder '" + logFolder.getAbsolutePath() + "'"); + System.exit(1); + } + } + Authentication authentication = new Authentication( user, diff --git a/src/main/java/life/qbic/io/parser/IdentifierParser.java b/src/main/java/life/qbic/io/parser/IdentifierParser.java index a6a7a55..33940ba 100644 --- a/src/main/java/life/qbic/io/parser/IdentifierParser.java +++ b/src/main/java/life/qbic/io/parser/IdentifierParser.java @@ -12,7 +12,7 @@ public class IdentifierParser { * Retrieve the identifiers from provided file * * @return Identifiers for which datasets will be retrieved - * @throws IOException + * @throws IOException if the file could not be read successfully */ public static List readProvidedIdentifiers(File file) throws IOException { List identifiers = new ArrayList<>(); diff --git a/src/main/java/life/qbic/model/Configuration.java b/src/main/java/life/qbic/model/Configuration.java index e3afdd5..cb567bd 100644 --- a/src/main/java/life/qbic/model/Configuration.java +++ b/src/main/java/life/qbic/model/Configuration.java @@ -8,6 +8,6 @@ */ public class Configuration { - public static long MAX_DOWNLOAD_ATTEMPTS = 3; - public static Path LOG_PATH = Paths.get(System.getProperty("user.dir"),"logs"); + public static final long MAX_DOWNLOAD_ATTEMPTS = 3; + public static final Path LOG_PATH = Paths.get(System.getProperty("user.dir"),"logs"); } diff --git a/src/main/java/life/qbic/model/download/Authentication.java b/src/main/java/life/qbic/model/download/Authentication.java index 35592d6..5a17c30 100644 --- a/src/main/java/life/qbic/model/download/Authentication.java +++ b/src/main/java/life/qbic/model/download/Authentication.java @@ -30,7 +30,7 @@ public Authentication( /** * Login method for openBIS authentication * - * returns 0 if successful, 1 else + * @throws AuthenticationException in case the authentication failed */ public void login() throws ConnectionException, AuthenticationException { try { diff --git a/src/main/java/life/qbic/model/download/QbicDataDisplay.java b/src/main/java/life/qbic/model/download/QbicDataDisplay.java index 7f12cdf..0cea822 100644 --- a/src/main/java/life/qbic/model/download/QbicDataDisplay.java +++ b/src/main/java/life/qbic/model/download/QbicDataDisplay.java @@ -6,27 +6,24 @@ import ch.ethz.sis.openbis.generic.dssapi.v3.IDataStoreServerApi; import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.DataSetFile; import ch.systemsx.cisd.common.spring.HttpInvokerUtils; +import life.qbic.model.files.FileSize; +import life.qbic.model.files.FileSizeFormatter; + import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.util.Comparator; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; -import life.qbic.model.files.FileSize; -import life.qbic.model.files.FileSizeFormatter; /** * Lists information about requested datasets and their files */ public class QbicDataDisplay { - String sessionToken; + final String sessionToken; - DateTimeFormatter utcDateTimeFormatterIso8601 = new DateTimeFormatterBuilder() + private final static DateTimeFormatter utcDateTimeFormatterIso8601 = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM-dd'T'hh:mm:ss") .appendZoneId() .toFormatter() @@ -38,7 +35,7 @@ public class QbicDataDisplay { * Constructor for a QbicDataDisplay instance * * @param AppServerUri The openBIS application server URL (AS) - * @param DataServerUri The openBIS datastore server URL (DSS) + * @param dataServerUris The openBIS datastore server URLs (DSS) * @param sessionToken The session token for the datastore & application servers */ public QbicDataDisplay( From dab7bf4528a1cade99fe776546531acc37e00518 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Mon, 20 Feb 2023 14:35:33 +0100 Subject: [PATCH 7/8] Tame progress bar (#152) * Tame progress bar * Incorporate feedback Co-authored-by: steffengreiner * Initialize last updated with 0 * Fix small error * simplify console output * make update interval constant --------- Co-authored-by: steffengreiner --- src/main/java/life/qbic/App.java | 4 +- .../model/download/QbicDataDownloader.java | 2 +- src/main/java/life/qbic/util/ProgressBar.java | 51 +++++++++++++------ src/main/resources/log4j2.xml | 2 +- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/main/java/life/qbic/App.java b/src/main/java/life/qbic/App.java index 5a7d155..e1206b8 100644 --- a/src/main/java/life/qbic/App.java +++ b/src/main/java/life/qbic/App.java @@ -11,6 +11,7 @@ import picocli.CommandLine; import java.io.File; +import java.util.Arrays; import java.util.Optional; /** @@ -21,11 +22,10 @@ public class App { private static final Logger LOG = LogManager.getLogger(App.class); public static void main(String[] args) { - + LOG.debug("command line arguments: " + Arrays.deepToString(args)); CommandLine cmd = new CommandLine(new PostmanCommandLineOptions()); int exitCode = cmd.execute(args); System.exit(exitCode); - } /** diff --git a/src/main/java/life/qbic/model/download/QbicDataDownloader.java b/src/main/java/life/qbic/model/download/QbicDataDownloader.java index b335e55..2d7e26f 100644 --- a/src/main/java/life/qbic/model/download/QbicDataDownloader.java +++ b/src/main/java/life/qbic/model/download/QbicDataDownloader.java @@ -311,10 +311,10 @@ private long writeFileToDisk(DataSetFile dataSetFile, IDataStoreServerApi dataSt int bytesRead; while ((bytesRead = checkedInputStream.read(buffer)) != -1) { progressBar.updateProgress(bufferSize); - progressBar.draw(); os.write(buffer, 0, bytesRead); os.flush(); } + progressBar.remove(); // flush OutputStream to write any buffered data to file os.flush(); diff --git a/src/main/java/life/qbic/util/ProgressBar.java b/src/main/java/life/qbic/util/ProgressBar.java index 08a6213..74ea198 100644 --- a/src/main/java/life/qbic/util/ProgressBar.java +++ b/src/main/java/life/qbic/util/ProgressBar.java @@ -1,24 +1,27 @@ package life.qbic.util; +import jline.TerminalFactory; +import life.qbic.model.files.FileSize; +import life.qbic.model.files.FileSizeFormatter; + import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; -import jline.TerminalFactory; -import life.qbic.model.files.FileSize; -import life.qbic.model.files.FileSizeFormatter; public class ProgressBar { private final int BARSIZE = TerminalFactory.get().getWidth() / 3; private final int MAXFILENAMESIZE = TerminalFactory.get().getWidth() / 3; + private static final long UPDATE_INTERVAL = 1000; private float nextProgressJump; private final float stepSize; private final String fileName; private final Long totalFileSize; private Long downloadedSize; private final long start; + private long lastUpdated; public ProgressBar(String fileName, long totalFileSize) { this.fileName = shortenFileName(fileName); @@ -27,11 +30,35 @@ public ProgressBar(String fileName, long totalFileSize) { this.stepSize = (float) totalFileSize / (float) BARSIZE; this.nextProgressJump = this.stepSize; this.start = System.currentTimeMillis(); + lastUpdated = 0; } public void updateProgress(int addDownloadedSize) { this.downloadedSize += (long) addDownloadedSize; - checkForJump(); + update(); + } + + /** + * Updates the progress bar if an update is applicable. + */ + public void update() { + if (progressStepsChanged()) { + this.nextProgressJump += this.stepSize; + drawProgress(); + } + // update periodically + if (isLastUpdateOutdated()) { + drawProgress(); + } + } + + private boolean progressStepsChanged() { + return this.downloadedSize > this.nextProgressJump; + } + + private boolean isLastUpdateOutdated() { + long timePassedSinceLastUpdate = System.currentTimeMillis() - lastUpdated; + return timePassedSinceLastUpdate >= UPDATE_INTERVAL; } private String shortenFileName(String fullFileName) { @@ -44,19 +71,13 @@ private String shortenFileName(String fullFileName) { return shortName; } - private void checkForJump() { - if (this.downloadedSize > this.nextProgressJump) { - this.nextProgressJump += this.stepSize; - drawProgress(); - } - } - - public void draw() { - drawProgress(); + private void drawProgress() { + System.out.printf("\r%-" + computeLeftPadding() + "s %s", this.fileName, buildProgressBar()); + lastUpdated = System.currentTimeMillis(); } - private void drawProgress() { - System.out.printf("%-" + computeLeftPadding() + "s %s\r", this.fileName, buildProgressBar()); + public void remove() { + System.out.printf("\r%"+TerminalFactory.get().getWidth()+"s\r", ""); //clear whole line } private int computeLeftPadding() { diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 5a64780..282f112 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ - + From 6b929b874167e8a2aa64bc8ae6456ad28d1073e3 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Mon, 20 Feb 2023 14:40:48 +0100 Subject: [PATCH 8/8] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 98fae50..787b955 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Parameters: [SAMPLE_ID...] one or more QBiC sample ids Options: + -V, --version print version information -u, --user= openBIS user name -p, --env-password= provide the name of an environment variable to read in the password from