From 414101fc7a944d5df0f265e1958086b929e90a3b Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Tue, 21 Jul 2026 13:50:29 +0200 Subject: [PATCH 01/11] ignore hidden directories when reading entire package collections/archives --- src/Poseidon/Core/Package.hs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Poseidon/Core/Package.hs b/src/Poseidon/Core/Package.hs index 9f9efc87..f7e7a546 100644 --- a/src/Poseidon/Core/Package.hs +++ b/src/Poseidon/Core/Package.hs @@ -709,11 +709,16 @@ checkJannoBibConsistency pacName (JannoRows rows) bibtex = do findAllPoseidonYmlFiles :: FilePath -> IO [FilePath] findAllPoseidonYmlFiles baseDir = do entries <- listDirectory baseDir - let posFiles = map (baseDir ) $ filter (=="POSEIDON.yml") $ map takeFileName entries - subDirs <- filterM doesDirectoryExist . map (baseDir ) $ entries + let posFiles = map (baseDir ) $ filter (=="POSEIDON.yml") entries + subDirs <- filterM doesDirectoryExist . map (baseDir ) $ filter (not . isHidden) entries morePosFiles <- fmap concat . mapM findAllPoseidonYmlFiles $ subDirs return $ posFiles ++ morePosFiles +isHidden :: FilePath -> Bool +isHidden p = case takeFileName p of + '.':_ -> True + _ -> False + -- | A function to read genotype data jointly from multiple packages getJointGenotypeData :: MonadSafe m => LogA -- ^ how messages should be logged From a1f0fe4eb8a38eec1e56b1189e997ee17f377157 Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Thu, 23 Jul 2026 11:19:10 +0200 Subject: [PATCH 02/11] html api: made display of description and source url indendent of each other --- src/Poseidon/Core/ServerHTML.hs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Poseidon/Core/ServerHTML.hs b/src/Poseidon/Core/ServerHTML.hs index 63c635b2..89aa0f0b 100644 --- a/src/Poseidon/Core/ServerHTML.hs +++ b/src/Poseidon/Core/ServerHTML.hs @@ -159,10 +159,13 @@ mainPage pacsPerArchive = do H.a ! A.href ("/explorer/" <> H.toValue archiveName <> "/" <> H.toValue pacName) $ H.toMarkup pacNameVersion H.toMarkup $ H.string $ " (" ++ maybe "?" show (posPacLastModified pac) ++ "); " -- archives with more info - case (maybeDescription,maybeURL) of - (Just desc, Just url) -> do + case maybeDescription of + Just desc -> do H.br H.p $ H.toMarkup desc + _ -> return () + case maybeURL of + Just url -> do H.footer $ H.p $ H.a ! A.href (H.stringValue url) ! A.style "float: right; font-size: 0.8em;" $ From 537fac0d039ee96cb97769b3c8158f96716a292e Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Thu, 23 Jul 2026 11:30:33 +0200 Subject: [PATCH 03/11] html api: make source column hide when url missing --- src/Poseidon/Core/ServerHTML.hs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Poseidon/Core/ServerHTML.hs b/src/Poseidon/Core/ServerHTML.hs index 89aa0f0b..7cca56aa 100644 --- a/src/Poseidon/Core/ServerHTML.hs +++ b/src/Poseidon/Core/ServerHTML.hs @@ -238,7 +238,9 @@ archivePage archiveName maybeArchiveSpecURL excludeFromMap plotSamples pacs = do H.th $ H.b "Package" H.th $ H.b "# Samples" H.th $ H.b "Last modified" - H.th $ H.b "Source" + case maybeArchiveSpecURL of + Just _ -> H.th $ H.b "Source" + Nothing -> return () H.th $ H.b ".zip Archive" forM_ pacs $ \pac -> do let pacName = getPacName pac @@ -253,8 +255,8 @@ archivePage archiveName maybeArchiveSpecURL excludeFromMap plotSamples pacs = do Nothing -> H.td $ H.string "n/a" -- archives with more info case maybeArchiveSpecURL of - Just url -> H.td $ H.a ! A.href (H.stringValue url <> "/" <> H.toValue pacName) $ H.toMarkup ("GitHub" :: String) - Nothing -> H.td $ H.string "n/a" + Just url -> H.td $ H.a ! A.href (H.stringValue url <> "/" <> H.toValue pacName) $ H.toMarkup ("URL" :: String) + Nothing -> return () H.td $ H.a ! A.href ("/zip_file/" <> H.toValue pacName <> "?archive=" <> H.toValue archiveName) $ H.toMarkup ("Download" :: String) packageVersionPage :: From da4b195bdbc08cc24764edc7303f6942c0cb6a2b Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Sun, 26 Jul 2026 22:24:54 +0200 Subject: [PATCH 04/11] implemented line ending validation with a warning as discussed in #311 --- src/Poseidon/CLI/Trident/Forge.hs | 4 +-- src/Poseidon/CLI/Trident/Init.hs | 2 +- src/Poseidon/Core/Package.hs | 27 ++++++++++++++----- src/Poseidon/Core/Utils.hs | 43 +++++++++++++++++++++++++++---- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/Poseidon/CLI/Trident/Forge.hs b/src/Poseidon/CLI/Trident/Forge.hs index b3b40336..d7551353 100644 --- a/src/Poseidon/CLI/Trident/Forge.hs +++ b/src/Poseidon/CLI/Trident/Forge.hs @@ -273,7 +273,7 @@ runForge ( (Just path) -> do logInfo "Copying README file from source package" let fullSourcePath = posPacBaseDir pacSource path - liftIO $ checkFile fullSourcePath Nothing + checkFile fullSourcePath Nothing liftIO $ copyFile fullSourcePath $ outPath path copyCHANGELOGFile :: FilePath -> PoseidonPackage -> PoseidonIO () copyCHANGELOGFile outPath pacSource = do @@ -282,7 +282,7 @@ runForge ( (Just path) -> do logInfo "Copying CHANGELOG file from source package" let fullSourcePath = posPacBaseDir pacSource path - liftIO $ checkFile fullSourcePath Nothing + checkFile fullSourcePath Nothing liftIO $ copyFile fullSourcePath $ outPath path compileGenotypeData :: FilePath -> GenotypeFileSpec -> [PoseidonPackage] -> [Int] -> PoseidonIO (VUM.IOVector Int) compileGenotypeData outPath gFileSpec relevantPackages relevantIndices = do diff --git a/src/Poseidon/CLI/Trident/Init.hs b/src/Poseidon/CLI/Trident/Init.hs index 754ab8cf..ca8bb93f 100644 --- a/src/Poseidon/CLI/Trident/Init.hs +++ b/src/Poseidon/CLI/Trident/Init.hs @@ -43,7 +43,7 @@ runInit (InitOptions genotypeDataIn outPathRaw maybeOutName minimal) = do GenotypePlink genoFile _ snpFile _ indFile _ -> [genoFile, snpFile, indFile] GenotypeVCF vcfFile _ -> [vcfFile] forM_ sourceFiles $ \sourceFile -> do - liftIO $ checkFile sourceFile Nothing + checkFile sourceFile Nothing let targetFile = outPath takeFileName sourceFile liftIO $ copyFile sourceFile targetFile -- create new package diff --git a/src/Poseidon/Core/Package.hs b/src/Poseidon/Core/Package.hs index f7e7a546..eac95537 100644 --- a/src/Poseidon/Core/Package.hs +++ b/src/Poseidon/Core/Package.hs @@ -82,6 +82,8 @@ import Poseidon.Core.ServerClient (AddColSpec (..), PackageInfo (..)) import Poseidon.Core.Utils (LogA, PoseidonException (..), PoseidonIO, checkFile, + checkLineEnding, + checkLineEndingIfNotZipped, envErrorLength, envLogAction, logDebug, logError, logInfo, logWarning, logWithEnv, @@ -443,7 +445,7 @@ readPoseidonPackage opts ymlPath = do checkYML yml -- file existence and checksum test - liftIO $ checkFiles baseDir (_readOptIgnoreChecksums opts) (_readOptIgnoreGeno opts) yml + checkFiles baseDir (_readOptIgnoreChecksums opts) (_readOptIgnoreGeno opts) yml -- read janno (or fill with empty dummy object) indEntries <- loadIndividuals baseDir geno @@ -552,7 +554,7 @@ validateForge pacs strandcheck = do ) (throwIO . PoseidonGenotypeExceptionForward errLength) -- throws exception if any file is missing or checksum is incorrect -checkFiles :: FilePath -> Bool -> Bool -> PoseidonYamlStruct -> IO () +checkFiles :: FilePath -> Bool -> Bool -> PoseidonYamlStruct -> PoseidonIO () checkFiles baseDir ignoreChecksums ignoreGenotypeFilesMissing yml = do -- Check README File case poseidonReadmeFilePath baseDir yml of @@ -567,19 +569,25 @@ checkFiles baseDir ignoreChecksums ignoreGenotypeFilesMissing yml = do Nothing -> return () Just fn -> if ignoreChecksums then checkFile fn Nothing - else checkFile fn $ _posYamlBibFileChkSum yml + else do + checkLineEnding fn + checkFile fn $ _posYamlBibFileChkSum yml -- Check Janno File case poseidonJannoFilePath baseDir yml of Nothing -> return () Just fn -> if ignoreChecksums then checkFile fn Nothing - else checkFile fn $ _posYamlJannoFileChkSum yml + else do + checkLineEnding fn + checkFile fn $ _posYamlJannoFileChkSum yml -- Check SeqSource File case poseidonSeqSourceFilePath baseDir yml of Nothing -> return () Just fn -> if ignoreChecksums then checkFile fn Nothing - else checkFile fn $ _posYamlSeqSourceFileChkSum yml + else do + checkLineEnding fn + checkFile fn $ _posYamlSeqSourceFileChkSum yml -- Check Genotype files unless ignoreGenotypeFilesMissing $ do let gd = _posYamlGenotypeData yml @@ -592,8 +600,11 @@ checkFiles baseDir ignoreChecksums ignoreGenotypeFilesMissing yml = do checkFile (d snpF) Nothing checkFile (d indF) Nothing else do + checkLineEndingIfNotZipped (d genoF) checkFile (d genoF) genoFc + checkLineEndingIfNotZipped (d snpF) checkFile (d snpF) snpFc + checkLineEndingIfNotZipped (d indF) checkFile (d indF) indFc GenotypePlink genoF genoFc snpF snpFc indF indFc -> do if ignoreChecksums @@ -603,12 +614,16 @@ checkFiles baseDir ignoreChecksums ignoreGenotypeFilesMissing yml = do checkFile (d indF) Nothing else do checkFile (d genoF) genoFc + checkLineEndingIfNotZipped (d snpF) checkFile (d snpF) snpFc + checkLineEndingIfNotZipped (d indF) checkFile (d indF) indFc GenotypeVCF genoF genoFc -> do if ignoreChecksums then checkFile (d genoF) Nothing - else checkFile (d genoF) genoFc + else do + checkLineEndingIfNotZipped (d genoF) + checkFile (d genoF) genoFc -- the last flag is important for reading VCFs, which can lack group and sex information. checkJannoIndConsistency :: String -> JannoRows -> [EigenstratIndEntry] -> Bool -> IO () diff --git a/src/Poseidon/Core/Utils.hs b/src/Poseidon/Core/Utils.hs index 0bd36d60..81e0e1d5 100644 --- a/src/Poseidon/Core/Utils.hs +++ b/src/Poseidon/Core/Utils.hs @@ -12,6 +12,8 @@ module Poseidon.Core.Utils ( envErrorLength, LogMode (..), checkFile, + checkLineEnding, + checkLineEndingIfNotZipped, getChecksum, logWarning, logInfo, @@ -38,17 +40,20 @@ import Colog (HasLog (..), LogAction (..), Message, msgText, showSeverity) import Control.Exception (Exception (..), throwIO) import Control.Exception.Base (SomeException) -import Control.Monad (when) +import Control.Monad (unless, when) import Control.Monad.Catch (throwM) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Reader (ReaderT, asks, runReaderT) +import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LB import Data.Digest.Pure.MD5 (md5) +import Data.List (isSuffixOf) import qualified Data.Set as Set import Data.Text (Text, pack) import Data.Time (defaultTimeLocale, formatTime, getCurrentTime, utcToLocalZonedTime) import Data.Version (showVersion) +import Data.Word (Word8) import Data.Yaml (ParseException, prettyPrintParseException) import GHC.Stack (callStack, withFrozenCallStack) @@ -56,9 +61,9 @@ import Network.HTTP.Conduit (HttpException (..)) import SequenceFormats.Plink (PlinkPopNameMode (..)) import System.Directory (doesFileExist) import System.FilePath.Posix (takeBaseName) +import System.IO (IOMode (..), withBinaryFile) import qualified Text.Parsec.Error as P - type LogA = LogAction IO Message data TestMode = Testing | Production deriving Show @@ -279,16 +284,16 @@ renderPoseidonException PoseidonCantPreserveException = "Can't use --preserve if there is more than one relevant source package." -- helper function to check if a file exists -checkFile :: FilePath -> Maybe String -> IO () +checkFile :: FilePath -> Maybe String -> PoseidonIO () checkFile fn maybeChkSum = do - fe <- doesFileExist fn + fe <- liftIO $ doesFileExist fn if not fe then throwM (PoseidonFileExistenceException fn) else case maybeChkSum of Nothing -> return () Just chkSum -> do - fnChkSum <- getChecksum fn + fnChkSum <- liftIO $ getChecksum fn when (fnChkSum /= chkSum) $ throwM (PoseidonFileChecksumException fn) -- helper functions to get the checksum of a file @@ -298,6 +303,34 @@ getChecksum f = do let md5Digest = md5 fileContent return $ show md5Digest +-- helper function to check line endings of text files +-- only considers the first line: +-- if the byte immediately before the first LF is CR, +-- then the first line uses CRLF; otherwise it uses plain Unix LF +checkLineEnding :: FilePath -> PoseidonIO () +checkLineEnding fn = + liftIO (withBinaryFile fn ReadMode $ \h -> go h Nothing) >>= \isCRLF -> + when isCRLF $ logWarning $ "File " <> fn <> " appears to use CRLF (Windows) line endings." + where + go h prev = do + chunk <- BS.hGetSome h 8192 + if BS.null chunk + then pure False + else case BS.elemIndex lf chunk of + Just i -> + if i > 0 + then pure $ BS.index chunk (i - 1) == cr + else pure $ prev == Just cr + Nothing -> + go h (Just $ BS.last chunk) + +lf, cr :: Word8 +lf = 10 -- '\n' +cr = 13 -- '\r' + +checkLineEndingIfNotZipped :: FilePath -> PoseidonIO () +checkLineEndingIfNotZipped fn = unless (".gz" `isSuffixOf` fn) $ checkLineEnding fn + -- helper functions to pad and cut strings padRight :: Int -> String -> String padRight n s From a64cd4c9a77fea5d43bd3d71b08a9a51b66583d0 Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Mon, 27 Jul 2026 22:52:37 +0200 Subject: [PATCH 05/11] allow ORCIDs in rectify --newContributors; solves #330 when merged --- poseidon-hs.cabal | 1 + .../CLI/Trident/OptparseApplicativeParsers.hs | 4 +- src/Poseidon/Core/Contributor.hs | 10 ++--- test/Poseidon/Core/ContributorSpec.hs | 37 +++++++++++++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 test/Poseidon/Core/ContributorSpec.hs diff --git a/poseidon-hs.cabal b/poseidon-hs.cabal index f83965c4..ae2c1040 100644 --- a/poseidon-hs.cabal +++ b/poseidon-hs.cabal @@ -85,6 +85,7 @@ Test-Suite poseidon-tools-tests Poseidon.Core.MathHelpersSpec, Poseidon.Core.JannocoalesceSpec, Poseidon.Core.SummariseSpec, Poseidon.Core.SurveySpec, Poseidon.Core.GenotypeDataSpec, Poseidon.Core.EntitiesListSpec, Poseidon.Core.ChronicleSpec, Poseidon.Core.SequencingSourceSpec, Poseidon.Core.InterfaceSpec, + Poseidon.Core.ContributorSpec -- analysis Poseidon.Analysis.FStatsSpec, Poseidon.Analysis.UtilsSpec default-language: Haskell2010 diff --git a/src/Poseidon/CLI/Trident/OptparseApplicativeParsers.hs b/src/Poseidon/CLI/Trident/OptparseApplicativeParsers.hs index aa20f731..48681602 100644 --- a/src/Poseidon/CLI/Trident/OptparseApplicativeParsers.hs +++ b/src/Poseidon/CLI/Trident/OptparseApplicativeParsers.hs @@ -234,7 +234,7 @@ parseMaybeContributors = OP.option (Just <$> OP.eitherReader readContributorStri OP.long "newContributors" <> OP.metavar "DSL" <> OP.help "Contributors to add to the POSEIDON.yml file \ - \ in the form \"[Firstname Lastname](Email address);...\"." <> + \ in the form \"[Firstname Lastname](Email address);...\". The ORCIDs are optional." <> OP.value Nothing ) @@ -243,7 +243,7 @@ parseContributors = OP.option (OP.eitherReader readContributorString) ( OP.long "newContributors" <> OP.metavar "DSL" <> OP.help "Contributors to add to the POSEIDON.yml file \ - \ in the form \"[Firstname Lastname](Email address);...\"." + \ in the form \"[Firstname Lastname](Email address);...\". The ORCIDs are optional." ) readContributorString :: String -> Either String [ContributorSpec] diff --git a/src/Poseidon/Core/Contributor.hs b/src/Poseidon/Core/Contributor.hs index 02f3a673..89636a3b 100644 --- a/src/Poseidon/Core/Contributor.hs +++ b/src/Poseidon/Core/Contributor.hs @@ -44,14 +44,14 @@ instance ToJSON ContributorSpec where ] contributorSpecParser :: P.Parser [ContributorSpec] -contributorSpecParser = P.try (P.sepBy oneContributorSpecParser (P.char ';' <* P.spaces)) +contributorSpecParser = P.try (P.sepBy oneContributorSpecParser (P.char ';' <* P.spaces)) <* P.spaces <* P.eof oneContributorSpecParser :: P.Parser ContributorSpec oneContributorSpecParser = do name <- P.between (P.char '[') (P.char ']') (P.manyTill P.anyChar (P.lookAhead (P.char ']'))) email <- P.between (P.char '(') (P.char ')') (P.manyTill P.anyChar (P.lookAhead (P.char ')'))) - -- TODO: add option to add ORCID here - return (ContributorSpec name email Nothing) + orcid <- P.optionMaybe $ P.between (P.char '<') (P.char '>') parseORCID + return (ContributorSpec name email orcid) -- | A data type to represent an ORCID -- see https://support.orcid.org/hc/en-us/articles/360006897674-Structure-of-the-ORCID-Identifier @@ -62,7 +62,7 @@ data ORCID = ORCID deriving (Show, Eq, Ord) instance FromJSON ORCID where - parseJSON (String s) = case P.runParser parseORCID () "" (unpack s) of + parseJSON (String s) = case P.runParser (parseORCID <* P.eof) () "" (unpack s) of Left err -> fail $ showParsecErr err Right x -> pure x parseJSON _ = mzero @@ -76,7 +76,7 @@ parseORCID = do fourBlock <* m <*> fourBlock <* m <*> fourBlock <* m - <*> threeBlock <*> checksumDigit <* P.eof + <*> threeBlock <*> checksumDigit guard (validateORCID orcid) P. "ORCID is not valid" return orcid where diff --git a/test/Poseidon/Core/ContributorSpec.hs b/test/Poseidon/Core/ContributorSpec.hs new file mode 100644 index 00000000..706577f5 --- /dev/null +++ b/test/Poseidon/Core/ContributorSpec.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Poseidon.Core.ContributorSpec (spec) where + +import Poseidon.Core.Contributor + +import Data.Either (isLeft) +import Test.Hspec +import qualified Text.Parsec as P +import Text.Parsec.Error (Message (..), newErrorMessage) +import Text.Parsec.Pos (newPos) + +spec :: Spec +spec = do + testContributorSpecParser + +testContributorSpecParser :: Spec +testContributorSpecParser = + describe "Poseidon.Core.Contributor.contributorSpecParser" $ do + it "should work for one individual" $ do + P.runParser contributorSpecParser () "" "[A B C](abc@hmail.com)" `shouldBe` + Right [ContributorSpec "A B C" "abc@hmail.com" Nothing] + it "should work for multiple individuals" $ do + P.runParser contributorSpecParser () "" "[A B C](abc@hmail.com);[D E F](def@hmail.com)" `shouldBe` + Right [ContributorSpec "A B C" "abc@hmail.com" Nothing, + ContributorSpec "D E F" "def@hmail.com" Nothing] + it "should work with ORCIDs" $ do + P.runParser contributorSpecParser () "" + "[A B C](abc@hmail.com)<0000-0002-1825-0097>;[D E F](def@hmail.com)<0000-0003-3448-5715>" `shouldBe` + Right [ContributorSpec "A B C" "abc@hmail.com" (Just $ ORCID "000000021825009" '7'), + ContributorSpec "D E F" "def@hmail.com" (Just $ ORCID "000000033448571" '5')] + it "should fail with wrong ORCIDs" $ do + P.runParser contributorSpecParser () "" "[A B C](abc@hmail.com)<0000-0003-3448-5716>;[D E F](def@hmail.com)" `shouldBe` + Left (newErrorMessage (Expect "ORCID is not valid") (newPos "" 1 43)) + it "should fail with trailing garbage" $ do + P.runParser contributorSpecParser () "" "[A B C](abc@hmail.com)Test;[D E F](def@hmail.com)" `shouldSatisfy` isLeft + P.runParser contributorSpecParser () "" "[A B C](abc@hmail.com);[D E F](def@hmail.com)Test" `shouldSatisfy` isLeft From 3f288f955efacd8b615c519449336607f6ee555d Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Mon, 3 Aug 2026 12:26:57 +0200 Subject: [PATCH 06/11] better handling of additional columns for trident list; should fix #380 --- src/Poseidon/CLI/Trident/List.hs | 6 +++--- src/Poseidon/CLI/Trident/Serve.hs | 6 +++--- src/Poseidon/Core/Package.hs | 15 ++++++++++----- src/Poseidon/Core/ServerClient.hs | 3 ++- .../GoldenTestCheckSumFile.txt | 3 ++- .../forge/ForgePac20/ForgePac20.janno | 6 +++--- .../genoconvert/out_vcf/Schiffels_2016.vcf.gz | Bin 495 -> 495 bytes .../GoldenTestData/list/list8 | 3 +++ .../GoldenTestsRunCommands.hs | 7 +++++++ .../Schmid_2028_utf8/Schmid_2028.janno | 6 +++--- 10 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 test/PoseidonGoldenTests/GoldenTestData/list/list8 diff --git a/src/Poseidon/CLI/Trident/List.hs b/src/Poseidon/CLI/Trident/List.hs index 8c34d4c3..287b065c 100644 --- a/src/Poseidon/CLI/Trident/List.hs +++ b/src/Poseidon/CLI/Trident/List.hs @@ -114,7 +114,7 @@ runList (ListOptions repoLocation listEntity rawOutput onlyLatest) = do let addJannoColFlag = case addJannoColSpec of AddColAll -> "&additionalJannoColumns=ALL" AddColList [] -> "" - AddColList moreJannoColumns -> "&additionalJannoColumns=" ++ intercalate "," moreJannoColumns + AddColList moreJannoColumns -> "&additionalJannoColumns=" ++ T.unpack (T.intercalate "," moreJannoColumns) apiReturn <- processApiResponse (remoteURL ++ "/individuals" ++ qDefault archive ++ addJannoColFlag) False case apiReturn of ApiReturnExtIndividualInfo indInfo -> return indInfo @@ -153,7 +153,7 @@ runList (ListOptions repoLocation listEntity rawOutput onlyLatest) = do let addJannoColFlag = case addColSpec of AddColAll -> "&additionalBibColumns=ALL" AddColList [] -> "" - AddColList moreBibFields -> "&additionalBibColumns=" ++ intercalate "," moreBibFields + AddColList moreBibFields -> "&additionalBibColumns=" ++ T.unpack (T.intercalate "," moreBibFields) apiReturn <- processApiResponse (remoteURL ++ "/bibliography" ++ qDefault archive ++ addJannoColFlag) False case apiReturn of ApiReturnBibInfo bibInfo -> return bibInfo @@ -164,7 +164,7 @@ runList (ListOptions repoLocation listEntity rawOutput onlyLatest) = do let addBibFieldNames = case addColSpec of AddColAll -> nub . concatMap (map fst . bibInfoAddCols) $ bibInfos - AddColList names -> names + AddColList names -> map T.unpack names -- warning in case the additional Columns do not exist in the entire janno dataset, -- we only output this warning if the columns were requested explicitly. Not if diff --git a/src/Poseidon/CLI/Trident/Serve.hs b/src/Poseidon/CLI/Trident/Serve.hs index 120e747e..af55782e 100644 --- a/src/Poseidon/CLI/Trident/Serve.hs +++ b/src/Poseidon/CLI/Trident/Serve.hs @@ -33,9 +33,9 @@ import Control.Monad (forM) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Coerce (coerce) import Data.List (groupBy, intercalate, sortOn) -import Data.List.Split (splitOn) import Data.Maybe (isJust) import Data.Scientific (toRealFloat) +import qualified Data.Text as T import Data.Text.Lazy (pack) import Data.Time (Day) import Data.Version (Version, parseVersion, @@ -227,7 +227,7 @@ runServer (ServeOptions archBaseDirs port ignoreChecksums certFiles) serverReady indInfo <- case maybeAdditionalColumnsString of Just "ALL" -> getExtendedIndividualInfo pacs AddColAll -- Nothing means all Janno Columns Just additionalColumnsString -> - let additionalColumnNames = splitOn "," additionalColumnsString + let additionalColumnNames = T.splitOn "," additionalColumnsString in getExtendedIndividualInfo pacs (AddColList additionalColumnNames) Nothing -> getExtendedIndividualInfo pacs (AddColList []) let retData = ApiReturnExtIndividualInfo indInfo @@ -241,7 +241,7 @@ runServer (ServeOptions archBaseDirs port ignoreChecksums certFiles) serverReady bibInfo <- case maybeAdditionalBibFieldsString of Just "ALL" -> getBibliographyInfo pacs AddColAll -- Nothing means all Janno Columns Just additionalBibFieldsString -> - let additionalBibFields = splitOn "," additionalBibFieldsString + let additionalBibFields = T.splitOn "," additionalBibFieldsString in getBibliographyInfo pacs (AddColList additionalBibFields) Nothing -> getBibliographyInfo pacs (AddColList []) let retData = ApiReturnBibInfo bibInfo diff --git a/src/Poseidon/Core/Package.hs b/src/Poseidon/Core/Package.hs index eac95537..dd71cab1 100644 --- a/src/Poseidon/Core/Package.hs +++ b/src/Poseidon/Core/Package.hs @@ -111,7 +111,8 @@ import Data.List (elemIndex, group, groupBy, (\\)) import Data.Maybe (catMaybes, fromMaybe, isNothing, mapMaybe) -import Data.Text (unpack) +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE import Data.Time (Day, UTCTime (..), getCurrentTime) import qualified Data.Vector as V @@ -1012,10 +1013,14 @@ getExtendedIndividualInfo allPackages addJannoColSpec = sequence $ do -- list mo let name = show $ jPoseidonID jannoRow groups = map show $ getListColumn . jGroupName $ jannoRow colNames = case addJannoColSpec of - AddColAll -> jannoHeaderString \\ ["Poseidon_ID", "Group_Name"] -- Nothing means all Janno columns + AddColAll -> map T.pack jannoHeaderString \\ ["Poseidon_ID", "Group_Name"] -- Nothing means all Janno columns -- except for these two which are already explicit AddColList c -> c - additionalColumnEntries = [(k, Bchs.unpack <$> toNamedRecord jannoRow HM.!? Bchs.pack k) | k <- colNames] + -- additionalColumnEntries = [(k, Bchs.unpack <$> toNamedRecord jannoRow HM.!? Bchs.pack k) | k <- colNames] + additionalColumnEntries = [ + (T.unpack k, T.unpack . TE.decodeUtf8 <$> HM.lookup (TE.encodeUtf8 k) (toNamedRecord jannoRow)) + | k <- colNames + ] isLatest <- isLatestInCollection allPackages pac -- this lives in monad m -- double-return for m and then list. return . return $ ExtendedIndividualInfo name groups (makePacNameAndVersion pac) isLatest additionalColumnEntries @@ -1038,14 +1043,14 @@ getBibliographyInfo allPackages addColSpec = do jannoRow <- jointJanno let bibKeys = case jPublication jannoRow of Nothing -> [] - Just jannoPubList -> map (\(JannoPublication p) -> unpack p) $ getListColumn jannoPubList + Just jannoPubList -> map (\(JannoPublication p) -> T.unpack p) $ getListColumn jannoPubList True <- return $ bibId `elem` bibKeys return () let addBibEntries = case addColSpec of -- with "all" we include all existing additional bib-entries except for the canonical ones that we anyway look up. AddColAll -> [(k, Just v) | (k, v) <- bibFields, k `notElem` ["title", "author", "year", "journal", "doi"]] -- with a selecton of colNames we just query the bib-fields for those exact fields. - AddColList colNames -> [(colName, colName `lookup` bibFields) | colName <- colNames] + AddColList colNames -> [(T.unpack colName, T.unpack colName `lookup` bibFields) | colName <- colNames] return $ BibliographyInfo nrSamples bibId ("title" `lookup` bibFields) ("author" `lookup` bibFields) ("year" `lookup` bibFields) ("journal" `lookup` bibFields) ("doi" `lookup` bibFields) addBibEntries diff --git a/src/Poseidon/Core/ServerClient.hs b/src/Poseidon/Core/ServerClient.hs index 0de2729b..0f7e83e3 100644 --- a/src/Poseidon/Core/ServerClient.hs +++ b/src/Poseidon/Core/ServerClient.hs @@ -31,6 +31,7 @@ import Data.Aeson (FromJSON, ToJSON (..), Value (..), toJSON, withObject, (.:), (.:?), (.=)) import qualified Data.Aeson.KeyMap (filter) +import qualified Data.Text as T import Data.Time (Day) import Data.Version (Version, showVersion) import Network.HTTP.Conduit (simpleHttp) @@ -259,7 +260,7 @@ extIndInfo2IndInfoCollection extIndInfos = in (indInfos, areLatest) -- type needed to specify additional Janno Columns to be queried from packages -data AddColSpec = AddColList [String] | AddColAll +data AddColSpec = AddColList [T.Text] | AddColAll data BibliographyInfo = BibliographyInfo { bibInfoNrSamples :: Int, diff --git a/test/PoseidonGoldenTests/GoldenTestCheckSumFile.txt b/test/PoseidonGoldenTests/GoldenTestCheckSumFile.txt index 4fc2ff38..73dff3db 100644 --- a/test/PoseidonGoldenTests/GoldenTestCheckSumFile.txt +++ b/test/PoseidonGoldenTests/GoldenTestCheckSumFile.txt @@ -34,6 +34,7 @@ d6eafec9087c88ed6a95e4a22f1f306c list list/list3 bc636b9c03ea9359acd254a9911e5af3 list list/list5 32c841b67d2995cdc1312f3721df5916 list list/list6 35689d85d4ac5da9a4189b728403f16b list list/list7 +848800d4b2604248f2e606a2f192994a list list/list8 b197fb8dd883c7469a4791e4a677f1c0 summarise summarise/summarise1 d9e4b3f15d4e129a365d2064198d95b6 summarise summarise/summarise2 5a5ff1b07bcc6ba917d5e492b886b71c survey survey/survey1 @@ -132,7 +133,7 @@ ad7e56177aad0a720f0bde13d47f2ac1 forge forge/ForgePac19/CHANGELOG.md 9b22cab26cdae87bd79d24c289e48433 forge forge/ForgePac19/ForgePac19.fam 504775c8994e25615b7662fc0495ff12 forge forge/ForgePac19/ForgePac19.janno 3288b6ff2d7b02eec0f34a8434e4b7e3 forge forge/ForgePac20/POSEIDON.yml -100405d0cc3bf72c1c3d7b87b7a22029 forge forge/ForgePac20/ForgePac20.janno +589e62622e1ee6f0ad6268935dd540ba forge forge/ForgePac20/ForgePac20.janno ef1646043331ed9d67e4072ee46eb9f4 forge forge/ForgePac21/POSEIDON.yml 8b8e1852b1e3cdd0ab0176dddce13bba forge forge/ForgePac21/ForgePac21.vcf 848cf0fab32e4078a88cc17ad8ec3381 forge forge/ForgePac21/ForgePac21.janno diff --git a/test/PoseidonGoldenTests/GoldenTestData/forge/ForgePac20/ForgePac20.janno b/test/PoseidonGoldenTests/GoldenTestData/forge/ForgePac20/ForgePac20.janno index 82bfc4a0..8849f1c3 100644 --- a/test/PoseidonGoldenTests/GoldenTestData/forge/ForgePac20/ForgePac20.janno +++ b/test/PoseidonGoldenTests/GoldenTestData/forge/ForgePac20/ForgePac20.janno @@ -1,3 +1,3 @@ -Poseidon_ID Genetic_Sex Group_Name Individual_ID Species Alternative_IDs Alternative_IDs_Context Relation_To Relation_Degree Relation_Type Collection_ID Custodian_Institution Cultural_Era Cultural_Era_URL Archaeological_Culture Archaeological_Culture_URL Country Country_ISO Location Site Latitude Longitude Date_Type Date_C14_Labnr Date_C14_Uncal_BP Date_C14_Uncal_BP_Err Date_BC_AD_Start Date_BC_AD_Median Date_BC_AD_Stop Chromosomal_Anomalies MT_Haplogroup Y_Haplogroup Source_Material Nr_Libraries Library_Names Capture_Type UDG Library_Built Genotype_Ploidy Data_Preparation_Pipeline_URL Endogenous Nr_SNPs Coverage_on_Target_SNPs Damage Contamination Contamination_Err Contamination_Meas Genetic_Source_Accession_IDs Primary_Contact Publication Note Keywords -XXX001 F POP1 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a testà n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a 9 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a -XXX002 F POP2 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a MAMS-47224 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a 9 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a +Poseidon_ID Genetic_Sex Group_Name Individual_ID Species Alternative_IDs Alternative_IDs_Context Relation_To Relation_Degree Relation_Type Collection_ID Custodian_Institution Cultural_Era Cultural_Era_URL Archaeological_Culture Archaeological_Culture_URL Country Country_ISO Location Site Latitude Longitude Date_Type Date_C14_Labnr Date_C14_Uncal_BP Date_C14_Uncal_BP_Err Date_BC_AD_Start Date_BC_AD_Median Date_BC_AD_Stop Chromosomal_Anomalies MT_Haplogroup Y_Haplogroup Source_Material Nr_Libraries Library_Names Capture_Type UDG Library_Built Genotype_Ploidy Data_Preparation_Pipeline_URL Endogenous Nr_SNPs Coverage_on_Target_SNPs Damage Contamination Contamination_Err Contamination_Meas Genetic_Source_Accession_IDs Primary_Contact Publication Note Keywords 起司蛋糕 +XXX001 F POP1 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a testà n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a 9 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a 鑽頭 +XXX002 F POP2 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a MAMS-47224 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a 9 n/a n/a n/a n/a n/a n/a n/a n/a n/a n/a 香腸沙拉 diff --git a/test/PoseidonGoldenTests/GoldenTestData/genoconvert/out_vcf/Schiffels_2016.vcf.gz b/test/PoseidonGoldenTests/GoldenTestData/genoconvert/out_vcf/Schiffels_2016.vcf.gz index 8ef6ceacaf50e2f842f13701643f8c56561955a1..4d23fd675770b8d15a4695cb0866463ccc5a8c9b 100644 GIT binary patch delta 404 zcmV;F0c-y61MdTnWq$@@=TzjkYn&w)Ub@?03#yoyDmf;v>F=&S&of@7hq&aw-L>kg z-63Z2#&Y8a?&sy)-Gu7qcKO8%7u4v_mAlKV;wN6Hb6y^b^jf8P=04#oN`6$r2GBufLSp?$NEQm)mvh~7{Y(f_(Sww_RD1iy|4*Ko| z%{CYXUC>$+34h)YtyXzRYc@kiu(Yld3(Hz6EOhdq0XW=csY$A~G9{2U6KGuk9XC+tQ~suXR0F`!U|*KMm77kNyI}3;8YE1ONa>q{)*2 delta 404 zcmV;F0c-y61MdTnWq(G+&Z)?6*EmZqymYt07F01YRdP&T)8Acvo@cyD4{^zVyKB`~ zyF<+4jpfD<+|SFoy9w3J?edEkE~wF;D|eS!#ZSCY=e#@=>9tDp%zeUFl>DfK$>sXW zOXX);ot1e~B~M=j&R?#g=UoFVO)8&V@v>B^bCM}0{Zu2$7JqS0?p%54L%<*c{)q^7 zYcFaLi|C=OP_#Ka$=C(Y@=9#hhi6}R!J2G?kSyn9_v{mI6_Ib>y%oV=r0y8OFOdCy zPbr`<6dlDtF;Og(DTp-n diff --git a/test/PoseidonGoldenTests/GoldenTestData/list/list8 b/test/PoseidonGoldenTests/GoldenTestData/list/list8 new file mode 100644 index 00000000..aa1c56bd --- /dev/null +++ b/test/PoseidonGoldenTests/GoldenTestData/list/list8 @@ -0,0 +1,3 @@ +Individual Group Package PackageVersion Is Latest 起司蛋糕 +XXX001 POP1 Schmid_2028 1.0.0 True 鑽頭 +XXX002 POP2 Schmid_2028 1.0.0 True 香腸沙拉 diff --git a/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs b/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs index a500711b..73257093 100644 --- a/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs +++ b/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs @@ -457,6 +457,13 @@ testPipelineList testDir checkFilePath = do } runAndChecksumStdOut checkFilePath testDir (testLog $ runList listOpts7) "list" 7 + let listOpts8 = listOpts1 { + _listRepoLocation = RepoLocal [testPacsDirOther "Schmid_2028_utf8"] + , _listListEntity = ListIndividuals (AddColList ["起司蛋糕"]) + , _listRawOutput = True + } + runAndChecksumStdOut checkFilePath testDir (testLog $ runList listOpts8) "list" 8 + testPipelineSummarise :: FilePath -> FilePath -> IO () testPipelineSummarise testDir checkFilePath = do diff --git a/test/testDat/testPackages/other_test_packages/Schmid_2028_utf8/Schmid_2028.janno b/test/testDat/testPackages/other_test_packages/Schmid_2028_utf8/Schmid_2028.janno index 2c26f32d..e439d8dc 100755 --- a/test/testDat/testPackages/other_test_packages/Schmid_2028_utf8/Schmid_2028.janno +++ b/test/testDat/testPackages/other_test_packages/Schmid_2028_utf8/Schmid_2028.janno @@ -1,3 +1,3 @@ -Poseidon_ID Group_Name Genetic_Sex Date_C14_Labnr -XXX001 POP1 F testà -XXX002 POP2 F MAMS-47224  +Poseidon_ID Group_Name Genetic_Sex Date_C14_Labnr 起司蛋糕 +XXX001 POP1 F testà 鑽頭 +XXX002 POP2 F MAMS-47224 香腸沙拉 From 9c3ddf8b89d4638c519c693856d9728188663ed2 Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Mon, 3 Aug 2026 14:37:21 +0200 Subject: [PATCH 07/11] another test to make sure the remote list behaviour described in #380 is also fixed, not just the local case --- test/PoseidonGoldenTests/GoldenTestCheckSumFile.txt | 1 + .../GoldenTestData/listRemote/listRemote6 | 3 +++ test/PoseidonGoldenTests/GoldenTestsRunCommands.hs | 13 ++++++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 test/PoseidonGoldenTests/GoldenTestData/listRemote/listRemote6 diff --git a/test/PoseidonGoldenTests/GoldenTestCheckSumFile.txt b/test/PoseidonGoldenTests/GoldenTestCheckSumFile.txt index 73dff3db..e43036b7 100644 --- a/test/PoseidonGoldenTests/GoldenTestCheckSumFile.txt +++ b/test/PoseidonGoldenTests/GoldenTestCheckSumFile.txt @@ -161,6 +161,7 @@ b43da4d5734371c0648553120f812466 fetch fetch/multi_packages_2/Lamnidis_2018-1.0. ed1d1343a7ff26274690c530a03cd070 listRemote listRemote/listRemote3 0433b2a80ee5a2eb5bf8c6404130e562 listRemote listRemote/listRemote4 e8be7325d86f75c8df826f91421e0239 listRemote listRemote/listRemote5 +848800d4b2604248f2e606a2f192994a listRemote listRemote/listRemote6 3ea2b421436a1be5cb631eaf6aa39546 jannocoalesce jannocoalesce/target1.janno e429b08154988f73c502921beb7b0842 jannocoalesce jannocoalesce/target2.janno a7983b6a2537513c43e5dc8105f6cec9 jannocoalesce jannocoalesce/target3.janno \ No newline at end of file diff --git a/test/PoseidonGoldenTests/GoldenTestData/listRemote/listRemote6 b/test/PoseidonGoldenTests/GoldenTestData/listRemote/listRemote6 new file mode 100644 index 00000000..aa1c56bd --- /dev/null +++ b/test/PoseidonGoldenTests/GoldenTestData/listRemote/listRemote6 @@ -0,0 +1,3 @@ +Individual Group Package PackageVersion Is Latest 起司蛋糕 +XXX001 POP1 Schmid_2028 1.0.0 True 鑽頭 +XXX002 POP2 Schmid_2028 1.0.0 True 香腸沙拉 diff --git a/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs b/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs index 73257093..8ba872f1 100644 --- a/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs +++ b/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs @@ -456,7 +456,7 @@ testPipelineList testDir checkFilePath = do _listListEntity = ListBibliography AddColAll } runAndChecksumStdOut checkFilePath testDir (testLog $ runList listOpts7) "list" 7 - + let listOpts8 = listOpts1 { _listRepoLocation = RepoLocal [testPacsDirOther "Schmid_2028_utf8"] , _listListEntity = ListIndividuals (AddColList ["起司蛋糕"]) @@ -1365,6 +1365,9 @@ archives = Left $ ArchiveConfig [ "test/testDat/testPackages/ancient/Schiffels_2016" , "test/testDat/testPackages/ancient/Schmid_2028" ] Nothing Nothing Nothing [] Nothing + , ArchiveSpec "testArchive3" [ + "test/testDat/testPackages/other_test_packages/Schmid_2028_utf8" + ] Nothing Nothing Nothing [] Nothing ] -- Note: We here use our test server (no SSL and different port). The reason is that @@ -1505,6 +1508,14 @@ testPipelineListRemote testDir checkFilePath = do , _listRawOutput = True } runAndChecksumStdOut checkFilePath testDir (testLog $ runList listOpts5) "listRemote" 5 + + -- list with non-ascii unicode characters + let listOpts6 = listOpts1 { + _listRepoLocation = RepoRemote (ArchiveEndpoint "http://localhost:3001" (Just "testArchive3")) + , _listListEntity = ListIndividuals (AddColList ["起司蛋糕"]) + , _listRawOutput = True + } + runAndChecksumStdOut checkFilePath testDir (testLog $ runList listOpts6) "listRemote" 6 ) ( killThread threadID ) From d261cd94217539d8ad728a4ed7a3a492879cc7b4 Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Mon, 3 Aug 2026 14:41:29 +0200 Subject: [PATCH 08/11] stylish-haskell --- test/PoseidonGoldenTests/GoldenTestsRunCommands.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs b/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs index 8ba872f1..ab2aaec0 100644 --- a/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs +++ b/test/PoseidonGoldenTests/GoldenTestsRunCommands.hs @@ -456,7 +456,7 @@ testPipelineList testDir checkFilePath = do _listListEntity = ListBibliography AddColAll } runAndChecksumStdOut checkFilePath testDir (testLog $ runList listOpts7) "list" 7 - + let listOpts8 = listOpts1 { _listRepoLocation = RepoLocal [testPacsDirOther "Schmid_2028_utf8"] , _listListEntity = ListIndividuals (AddColList ["起司蛋糕"]) @@ -1508,7 +1508,7 @@ testPipelineListRemote testDir checkFilePath = do , _listRawOutput = True } runAndChecksumStdOut checkFilePath testDir (testLog $ runList listOpts5) "listRemote" 5 - + -- list with non-ascii unicode characters let listOpts6 = listOpts1 { _listRepoLocation = RepoRemote (ArchiveEndpoint "http://localhost:3001" (Just "testArchive3")) From 345ef603047070a1089d6935cf2488d670b4ac8f Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Sun, 9 Aug 2026 12:31:11 +0200 Subject: [PATCH 09/11] update of changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5008d888..904efc84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +- V 2.2.1.0: + - Made the display of description and source url in the explorer page of the of the server's html API indendent of each other. + - Made the source column hide when the necessary URL is missing on the archive page. + - Added a input validation feature that checks line endings of the first (!) row in text files in packages. + - Made it possible to give ORCIDs with `--newContributors` in rectify. + - Fixed encoding issues in `trident list` when additional .janno columns are requested. - V 2.2.0.1: - By default only show the first five samples on the package page of the server's html API. - V 2.2.0.0: From 0de9ddb5a8c4ee82d6254e1a5040f77014735f3c Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Sun, 9 Aug 2026 15:26:14 +0200 Subject: [PATCH 10/11] calling checkfile in validate for nicer error messages with wrong file paths --- CHANGELOG.md | 3 +- src/Poseidon/CLI/Trident/Validate.hs | 9 +++- src/Poseidon/Core/Package.hs | 64 +++++++++++++--------------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 904efc84..915fa221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ - V 2.2.1.0: - Made the display of description and source url in the explorer page of the of the server's html API indendent of each other. - Made the source column hide when the necessary URL is missing on the archive page. - - Added a input validation feature that checks line endings of the first (!) row in text files in packages. + - Added a input validation feature that checks line endings of the first (!) row in text files in packages. Only runs when checksums are present, because only then a difference in line endings can lead to unexpected behaviour. - Made it possible to give ORCIDs with `--newContributors` in rectify. - Fixed encoding issues in `trident list` when additional .janno columns are requested. + - Nicer error messages in case of missing input files in `validate`. - V 2.2.0.1: - By default only show the first five samples on the package page of the server's html API. - V 2.2.0.0: diff --git a/src/Poseidon/CLI/Trident/Validate.hs b/src/Poseidon/CLI/Trident/Validate.hs index ebf87e11..44bc120e 100644 --- a/src/Poseidon/CLI/Trident/Validate.hs +++ b/src/Poseidon/CLI/Trident/Validate.hs @@ -10,6 +10,7 @@ import Poseidon.Core.Janno (JannoRows (..), readJannoFile) import Poseidon.Core.Package (PackageReadOptions (..), PoseidonException (..), PoseidonYamlStruct (..), + checkGenoFiles, defaultPackageReadOptions, getJointIndividualInfo, makePseudoPackageFromGenotypeData, @@ -17,7 +18,8 @@ import Poseidon.Core.Package (PackageReadOptions (..), validateForge, validateGeno) import Poseidon.Core.SequencingSource (SeqSourceRows (..), readSeqSourceFile) -import Poseidon.Core.Utils (PoseidonIO, logError, logInfo) +import Poseidon.Core.Utils (PoseidonIO, checkFile, + logError, logInfo) import Control.Monad (forM_, unless, when) import Control.Monad.Catch (throwM) @@ -96,6 +98,7 @@ runValidate (ValidateOptions conclude (not packagesSkipped) noExitCode runValidate (ValidateOptions (ValPlanPoseidonYaml path) _ _ noExitCode _) = do logInfo $ "Validating: " ++ path + checkFile path Nothing bs <- liftIO $ B.readFile path yml <- case decodeEither' bs of Left err -> throwM $ PoseidonYamlParseException path err @@ -108,21 +111,25 @@ runValidate (ValidateOptions (ValPlanGeno geno) _ _ noExitCode _) = do GenotypePlink gf _ _ _ _ _ -> gf GenotypeVCF gf _ -> gf logInfo $ "Validating: " ++ gFile + checkGenoFiles True "" (genotypeFileSpec geno) pac <- makePseudoPackageFromGenotypeData geno validateGeno pac True conclude True noExitCode runValidate (ValidateOptions (ValPlanJanno (VersionedFile pv path)) mandatoryJannoCols _ noExitCode _) = do logInfo $ "Validating: " ++ path + checkFile path Nothing (_,JannoRows entries) <- readJannoFile pv mandatoryJannoCols path logInfo $ "All " ++ show (length entries) ++ " entries are valid" conclude True noExitCode runValidate (ValidateOptions (ValPlanSSF (VersionedFile pv path)) _ mandatorySSFCols noExitCode _) = do logInfo $ "Validating: " ++ path + checkFile path Nothing (SeqSourceRows entries) <- readSeqSourceFile pv mandatorySSFCols path logInfo $ "All " ++ show (length entries) ++ " entries are valid" conclude True noExitCode runValidate (ValidateOptions (ValPlanBib path) _ _ noExitCode _) = do logInfo $ "Validating: " ++ path + checkFile path Nothing entries <- liftIO $ readBibTeXFile path logInfo $ "All " ++ show (length entries) ++ " entries are valid" conclude True noExitCode diff --git a/src/Poseidon/Core/Package.hs b/src/Poseidon/Core/Package.hs index dd71cab1..467051c4 100644 --- a/src/Poseidon/Core/Package.hs +++ b/src/Poseidon/Core/Package.hs @@ -10,6 +10,7 @@ module Poseidon.Core.Package ( LicenseSpec (..), findAllPoseidonYmlFiles, checkJannoIndConsistency, + checkGenoFiles, readPoseidonPackageCollection, readPoseidonPackageCollectionWithSkipIndicator, getJointGenotypeData, @@ -591,40 +592,35 @@ checkFiles baseDir ignoreChecksums ignoreGenotypeFilesMissing yml = do checkFile fn $ _posYamlSeqSourceFileChkSum yml -- Check Genotype files unless ignoreGenotypeFilesMissing $ do - let gd = _posYamlGenotypeData yml - d = baseDir - case genotypeFileSpec gd of - GenotypeEigenstrat genoF genoFc snpF snpFc indF indFc -> do - if ignoreChecksums - then do - checkFile (d genoF) Nothing - checkFile (d snpF) Nothing - checkFile (d indF) Nothing - else do - checkLineEndingIfNotZipped (d genoF) - checkFile (d genoF) genoFc - checkLineEndingIfNotZipped (d snpF) - checkFile (d snpF) snpFc - checkLineEndingIfNotZipped (d indF) - checkFile (d indF) indFc - GenotypePlink genoF genoFc snpF snpFc indF indFc -> do - if ignoreChecksums - then do - checkFile (d genoF) Nothing - checkFile (d snpF) Nothing - checkFile (d indF) Nothing - else do - checkFile (d genoF) genoFc - checkLineEndingIfNotZipped (d snpF) - checkFile (d snpF) snpFc - checkLineEndingIfNotZipped (d indF) - checkFile (d indF) indFc - GenotypeVCF genoF genoFc -> do - if ignoreChecksums - then checkFile (d genoF) Nothing - else do - checkLineEndingIfNotZipped (d genoF) - checkFile (d genoF) genoFc + checkGenoFiles ignoreChecksums baseDir (genotypeFileSpec (_posYamlGenotypeData yml)) + +checkGenoFiles :: Bool -> FilePath -> GenotypeFileSpec -> PoseidonIO () +checkGenoFiles True d (GenotypeEigenstrat genoF _ snpF _ indF _) = do + checkFile (d genoF) Nothing + checkFile (d snpF) Nothing + checkFile (d indF) Nothing +checkGenoFiles False d (GenotypeEigenstrat genoF genoFc snpF snpFc indF indFc) = do + checkLineEndingIfNotZipped (d genoF) + checkFile (d genoF) genoFc + checkLineEndingIfNotZipped (d snpF) + checkFile (d snpF) snpFc + checkLineEndingIfNotZipped (d indF) + checkFile (d indF) indFc +checkGenoFiles True d (GenotypePlink genoF _ snpF _ indF _) = do + checkFile (d genoF) Nothing + checkFile (d snpF) Nothing + checkFile (d indF) Nothing +checkGenoFiles False d (GenotypePlink genoF genoFc snpF snpFc indF indFc) = do + checkFile (d genoF) genoFc + checkLineEndingIfNotZipped (d snpF) + checkFile (d snpF) snpFc + checkLineEndingIfNotZipped (d indF) + checkFile (d indF) indFc +checkGenoFiles True d (GenotypeVCF genoF _) = do + checkFile (d genoF) Nothing +checkGenoFiles False d (GenotypeVCF genoF genoFc) = do + checkLineEndingIfNotZipped (d genoF) + checkFile (d genoF) genoFc -- the last flag is important for reading VCFs, which can lack group and sex information. checkJannoIndConsistency :: String -> JannoRows -> [EigenstratIndEntry] -> Bool -> IO () From 38d4045f3b45b203736794fe014f1878cd555454 Mon Sep 17 00:00:00 2001 From: Clemens Schmid Date: Tue, 11 Aug 2026 13:30:45 +0200 Subject: [PATCH 11/11] version bump --- poseidon-hs.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poseidon-hs.cabal b/poseidon-hs.cabal index ae2c1040..ed185b9b 100644 --- a/poseidon-hs.cabal +++ b/poseidon-hs.cabal @@ -1,5 +1,5 @@ name: poseidon-hs -version: 2.2.0.1 +version: 2.2.1.0 synopsis: A package with tools for working with Poseidon genotype data description: The tools in this package read and analyse Poseidon-formatted genotype databases, a modular system for storing genotype data from thousands of individuals. license: MIT