packages feed

hledger-flow 0.14.1.0 → 0.14.2.0

raw patch · 15 files changed

+399/−272 lines, 15 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Hledger.Flow.CSVImport: importCSVs :: RuntimeOptions -> IO ()
- Hledger.Flow.Common: channelErr :: TChan LogMessage -> Text -> IO ()
- Hledger.Flow.Common: channelErrLn :: TChan LogMessage -> Text -> IO ()
- Hledger.Flow.Common: channelOut :: TChan LogMessage -> Text -> IO ()
- Hledger.Flow.Common: channelOutLn :: TChan LogMessage -> Text -> IO ()
- Hledger.Flow.Common: consoleChannelLoop :: TChan LogMessage -> IO ()
- Hledger.Flow.Common: dummyLogger :: TChan LogMessage -> Text -> IO ()
- Hledger.Flow.Common: logToChannel :: TChan LogMessage -> Text -> IO ()
- Hledger.Flow.Common: logVerbose :: HasVerbosity o => o -> TChan LogMessage -> Text -> IO ()
- Hledger.Flow.Common: terminateChannelLoop :: TChan LogMessage -> IO ()
- Hledger.Flow.Common: timestampPrefix :: Text -> IO Text
+ Hledger.Flow.DateTime: currentDate :: IO (Integer, Int, Int)
+ Hledger.Flow.DateTime: currentYear :: IO Integer
+ Hledger.Flow.Import.CSVImport: importCSVs :: RuntimeOptions -> IO ()
+ Hledger.Flow.Import.ImportHelpers: findInputFiles :: Integer -> AbsDir -> IO [AbsFile]
+ Hledger.Flow.Import.ImportHelpers: findJournalFiles :: AbsDir -> IO [AbsFile]
+ Hledger.Flow.Logging: channelErr :: TChan LogMessage -> Text -> IO ()
+ Hledger.Flow.Logging: channelErrLn :: TChan LogMessage -> Text -> IO ()
+ Hledger.Flow.Logging: channelOut :: TChan LogMessage -> Text -> IO ()
+ Hledger.Flow.Logging: channelOutLn :: TChan LogMessage -> Text -> IO ()
+ Hledger.Flow.Logging: consoleChannelLoop :: TChan LogMessage -> IO ()
+ Hledger.Flow.Logging: dummyLogger :: TChan LogMessage -> Text -> IO ()
+ Hledger.Flow.Logging: logToChannel :: TChan LogMessage -> Text -> IO ()
+ Hledger.Flow.Logging: logVerbose :: HasVerbosity o => o -> TChan LogMessage -> Text -> IO ()
+ Hledger.Flow.Logging: terminateChannelLoop :: TChan LogMessage -> IO ()
+ Hledger.Flow.Logging: timestampPrefix :: Text -> IO Text
+ Hledger.Flow.PathHelpers: excludeHiddenFiles :: [AbsFile] -> [AbsFile]
+ Hledger.Flow.PathHelpers: findFilesIn :: MonadIO m => (AbsDir -> Bool) -> [RelDir] -> AbsDir -> m [AbsFile]
+ Hledger.Flow.RuntimeOptions: [importStartYear] :: RuntimeOptions -> Maybe Integer
- Hledger.Flow.RuntimeOptions: RuntimeOptions :: BaseDir -> RunDir -> Bool -> Text -> HledgerInfo -> SystemInfo -> Bool -> Bool -> Bool -> RuntimeOptions
+ Hledger.Flow.RuntimeOptions: RuntimeOptions :: BaseDir -> RunDir -> Maybe Integer -> Bool -> Text -> HledgerInfo -> SystemInfo -> Bool -> Bool -> Bool -> RuntimeOptions

Files

ChangeLog.md view
@@ -1,5 +1,15 @@ # Changelog for [hledger-flow](https://github.com/apauley/hledger-flow) +## 0.14.2++Add an optional `--start-year` command-line option for imports:++Import only from the specified year and onwards,+ignoring previous years. Valid values include a 4-digit+year or 'current' for the current year.++An implementation for [this feature request](https://github.com/apauley/hledger-flow/issues/81)+ ## 0.14.1  - Make `--enable-future-rundir` the default, and deprecate the command-line option. To be removed in a future release.
app/Main.hs view
@@ -3,6 +3,7 @@  module Main where +import Parsing import Path import qualified Turtle hiding (switch) import Prelude hiding (putStrLn)@@ -14,17 +15,18 @@ import Hledger.Flow.BaseDir import qualified Hledger.Flow.RuntimeOptions as RT import Hledger.Flow.Reports-import Hledger.Flow.CSVImport+import Hledger.Flow.Import.CSVImport  import Control.Monad (when) import qualified Data.Text.IO as T  data ImportParams = ImportParams { maybeImportBaseDir :: Maybe TurtlePath-                                 , importUseRunDir :: Bool+                                 , importStartYear :: Maybe String                                  , onlyNewFiles :: Bool+                                 , importUseRunDir :: Bool                                  } deriving (Show) -data ReportParams = ReportParams { maybeReportBaseDir :: Maybe TurtlePath } deriving (Show)+newtype ReportParams = ReportParams {maybeReportBaseDir :: Maybe TurtlePath} deriving Show  data Command = Import ImportParams | Report ReportParams deriving (Show) @@ -45,6 +47,7 @@  toRuntimeOptionsImport :: MainParams -> ImportParams -> IO RT.RuntimeOptions toRuntimeOptionsImport mainParams' subParams' = do+  startYear <- parseStartYear $ importStartYear subParams'   let maybeBD = maybeImportBaseDir subParams' :: Maybe TurtlePath   Control.Monad.when (importUseRunDir subParams') $ do     T.putStrLn "The enable-future-rundir option is now the default, no need to specify it. This option is currently being ignored and will be removed in future."@@ -52,6 +55,7 @@   hli <- hledgerInfoFromPath $ hledgerPathOpt mainParams'   return RT.RuntimeOptions { RT.baseDir = bd                            , RT.importRunDir = runDir+                           , RT.importStartYear = startYear                            , RT.onlyNewFiles = onlyNewFiles subParams'                            , RT.hfVersion = versionInfo'                            , RT.hledgerInfo = hli@@ -67,6 +71,7 @@   hli <- hledgerInfoFromPath $ hledgerPathOpt mainParams'   return RT.RuntimeOptions { RT.baseDir = bd                            , RT.importRunDir = [reldir|.|]+                           , RT.importStartYear = Nothing                            , RT.onlyNewFiles = False                            , RT.hfVersion = versionInfo'                            , RT.hledgerInfo = hli@@ -93,8 +98,9 @@ subcommandParserImport :: Parser ImportParams subcommandParserImport = ImportParams   <$> optional (Turtle.argPath "dir" "The directory to import. Use the base directory for a full import or a sub-directory for a partial import. Defaults to the current directory. This behaviour is changing: see --enable-future-rundir")-  <*> switch (long "enable-future-rundir" <> help "This switch is currently being ignored, since the behaviour it previously enabled is now the default. It will be removed in future.")+  <*> optional (strOption (long "start-year" <> metavar "YEAR" <> help "Import only from the specified year and onwards, ignoring previous years. By default all available years are imported. Valid values include a 4-digit year or 'current' for the current year"))   <*> switch (long "new-files-only" <> help "Don't regenerate transaction files if they are already present. This applies to hledger journal files as well as files produced by the preprocess and construct scripts.")+  <*> switch (long "enable-future-rundir" <> help "This switch is currently being ignored, since the behaviour it previously enabled is now the default. It will be removed in future.")  subcommandParserReport :: Parser ReportParams subcommandParserReport = ReportParams
+ app/Parsing.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE PartialTypeSignatures #-}++module Parsing (+    parseStartYear+  ) where++import Hledger.Flow.DateTime+import System.Exit (die)+import Text.Read (readMaybe)++parseStartYear :: Maybe String -> IO (Maybe Integer)+parseStartYear y = case y of+  Nothing -> return Nothing+  Just "current"  -> Just <$> currentYear+  Just s -> Just <$> parseInt s "Unable to parse year"++parseInt :: String -> String -> IO Integer+parseInt s errPrefix = case safeParseInt s errPrefix of+  Right i -> return i+  Left err -> die err++safeParseInt :: String -> String -> Either String Integer+safeParseInt s errPrefix = case (readMaybe s :: Maybe Integer) of+  Nothing -> Left $ errPrefix ++ " " ++ s+  Just i  -> Right i
hledger-flow.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: a351ed4904c5177d5fa526e91057902c42ef97c4bc7814624f684725f3f17abd+-- hash: cb2578ca8c6701d18ddc78599992215ad8c3b4f13ee2deb248bd30112365d3e4  name:           hledger-flow-version:        0.14.1.0+version:        0.14.2.0 synopsis:       An hledger workflow focusing on automated statement import and classification. description:    Please see the README on GitHub at <https://github.com/apauley/hledger-flow#readme> category:       Finance, Console@@ -31,9 +31,12 @@   exposed-modules:       Hledger.Flow.BaseDir       Hledger.Flow.Common-      Hledger.Flow.CSVImport+      Hledger.Flow.DateTime       Hledger.Flow.DocHelpers+      Hledger.Flow.Import.CSVImport+      Hledger.Flow.Import.ImportHelpers       Hledger.Flow.Import.Types+      Hledger.Flow.Logging       Hledger.Flow.PathHelpers       Hledger.Flow.Reports       Hledger.Flow.RuntimeOptions@@ -59,6 +62,7 @@ executable hledger-flow   main-is: Main.hs   other-modules:+      Parsing       Paths_hledger_flow   hs-source-dirs:       app
− src/Hledger/Flow/CSVImport.hs
@@ -1,210 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Hledger.Flow.CSVImport-    ( importCSVs-    ) where--import qualified Turtle hiding (stdout, stderr, proc, procStrictWithErr)-import Turtle ((%), (</>), (<.>))-import Prelude hiding (putStrLn, take)-import qualified Data.Text as T-import qualified Data.List.NonEmpty as NonEmpty-import qualified Hledger.Flow.Types as FlowTypes-import Hledger.Flow.Import.Types-import Hledger.Flow.BaseDir (relativeToBase, effectiveRunDir)-import Hledger.Flow.PathHelpers (TurtlePath, pathToTurtle)-import Hledger.Flow.DocHelpers (docURL)-import Hledger.Flow.Common-import Hledger.Flow.RuntimeOptions-import Control.Concurrent.STM-import Control.Monad--type FileWasGenerated = Bool--importCSVs :: RuntimeOptions -> IO ()-importCSVs opts = Turtle.sh (-  do-    ch <- Turtle.liftIO newTChanIO-    logHandle <- Turtle.fork $ consoleChannelLoop ch-    Turtle.liftIO $ if (showOptions opts) then channelOutLn ch (Turtle.repr opts) else return ()-    Turtle.liftIO $ logVerbose opts ch "Starting import"-    (journals, diff) <- Turtle.time $ Turtle.liftIO $ importCSVs' opts ch-    let generatedJournals = filter snd journals-    Turtle.liftIO $ channelOutLn ch $ Turtle.format ("Imported "%Turtle.d%"/"%Turtle.d%" journals in "%Turtle.s) (length generatedJournals) (length journals) $ Turtle.repr diff-    Turtle.liftIO $ terminateChannelLoop ch-    Turtle.wait logHandle-  )--pathSeparators :: [Char]-pathSeparators = ['/', '\\', ':']--inputFilePattern :: Turtle.Pattern T.Text-inputFilePattern = Turtle.contains (Turtle.once (Turtle.oneOf pathSeparators) <> Turtle.asciiCI "1-in" <> Turtle.once (Turtle.oneOf pathSeparators) <> Turtle.plus Turtle.digit <> Turtle.once (Turtle.oneOf pathSeparators))--importCSVs' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> IO [(TurtlePath, FileWasGenerated)]-importCSVs' opts ch = do-  let effectiveDir = effectiveRunDir (baseDir opts) (importRunDir opts)-  channelOutLn ch $ Turtle.format ("Collecting input files from "%Turtle.fp) $ pathToTurtle effectiveDir-  (inputFiles, diff) <- Turtle.time $ Turtle.single . shellToList . onlyFiles $ Turtle.find inputFilePattern (pathToTurtle effectiveDir)-  let fileCount = length inputFiles-  if (fileCount == 0) then-    do-      let msg = Turtle.format ("I couldn't find any input files underneath "%Turtle.fp-                        %"\n\nhledger-flow expects to find its input files in specifically\nnamed directories.\n\n"%-                        "Have a look at the documentation for a detailed explanation:\n"%Turtle.s) (pathToTurtle effectiveDir) (docURL "input-files")-      errExit 1 ch msg []-    else-    do-      channelOutLn ch $ Turtle.format ("Found "%Turtle.d%" input files in "%Turtle.s%". Proceeding with import...") fileCount (Turtle.repr diff)-      let actions = map (extractAndImport opts ch) inputFiles :: [IO (TurtlePath, FileWasGenerated)]-      importedJournals <- parAwareActions opts actions-      _ <- writeIncludesUpTo opts ch (pathToTurtle effectiveDir) $ fmap fst importedJournals-      _ <- writeToplevelAllYearsInclude opts-      return importedJournals--extractAndImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> IO (TurtlePath, FileWasGenerated)-extractAndImport opts ch inputFile = do-  case extractImportDirs inputFile of-    Right importDirs -> importCSV opts ch importDirs inputFile-    Left errorMessage -> do-      errExit 1 ch errorMessage (inputFile, False)--importCSV :: RuntimeOptions -> TChan FlowTypes.LogMessage -> ImportDirs -> TurtlePath -> IO (TurtlePath, FileWasGenerated)-importCSV opts ch importDirs srcFile = do-  let preprocessScript = accountDir importDirs </> "preprocess"-  let constructScript = accountDir importDirs </> "construct"-  let bankName = importDirLine bankDir importDirs-  let accountName = importDirLine accountDir importDirs-  let ownerName = importDirLine ownerDir importDirs-  (csvFile, preprocessHappened) <- preprocessIfNeeded opts ch preprocessScript bankName accountName ownerName srcFile-  let journalOut = changePathAndExtension "3-journal" "journal" csvFile-  shouldImport <- if onlyNewFiles opts && not preprocessHappened-    then not <$> verboseTestFile opts ch journalOut-    else return True--  importFun <- if shouldImport-    then constructOrImport opts ch constructScript bankName accountName ownerName-    else do-      _ <- logNewFileSkip opts ch "import" journalOut-      return $ \_p1 _p2 -> return journalOut-  Turtle.mktree $ Turtle.directory journalOut-  out <- importFun csvFile journalOut-  return (out, shouldImport)--constructOrImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> IO (TurtlePath -> TurtlePath -> IO TurtlePath)-constructOrImport opts ch constructScript bankName accountName ownerName = do-  constructScriptExists <- verboseTestFile opts ch constructScript-  if constructScriptExists-    then return $ customConstruct opts ch constructScript bankName accountName ownerName-    else return $ hledgerImport opts ch--preprocessIfNeeded :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> TurtlePath -> IO (TurtlePath, Bool)-preprocessIfNeeded opts ch script bank account owner src = do-  let csvOut = changePathAndExtension "2-preprocessed" "csv" src-  scriptExists <- verboseTestFile opts ch script-  shouldProceed <- if onlyNewFiles opts -    then do-      targetExists <- verboseTestFile opts ch csvOut-      return $ scriptExists && not targetExists-    else return scriptExists-  if shouldProceed-    then do-     out <- preprocess opts ch script bank account owner src csvOut-     return (out, True)-    else do-      _ <- logNewFileSkip opts ch "preprocess" csvOut-      return (src, False)--logNewFileSkip :: RuntimeOptions -> TChan FlowTypes.LogMessage -> T.Text -> TurtlePath -> IO ()-logNewFileSkip opts ch logIdentifier absTarget =-  Control.Monad.when (onlyNewFiles opts) $ do-   let relativeTarget = relativeToBase opts absTarget-   logVerbose opts ch-     $ Turtle.format-        ("Skipping " % Turtle.s-         % " - only creating new files and this output file already exists: '"-         % Turtle.fp-         % "'") logIdentifier relativeTarget--preprocess :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> TurtlePath -> TurtlePath -> IO TurtlePath-preprocess opts ch script bank account owner src csvOut = do-  Turtle.mktree $ Turtle.directory csvOut-  let args = [Turtle.format Turtle.fp src, Turtle.format Turtle.fp csvOut, Turtle.lineToText bank, Turtle.lineToText account, Turtle.lineToText owner]-  let relScript = relativeToBase opts script-  let relSrc = relativeToBase opts src-  let cmdLabel = Turtle.format ("executing '"%Turtle.fp%"' on '"%Turtle.fp%"'") relScript relSrc-  _ <- timeAndExitOnErr opts ch cmdLabel channelOut channelErr (parAwareProc opts) (Turtle.format Turtle.fp script, args, Turtle.empty)-  return csvOut--hledgerImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> TurtlePath -> IO TurtlePath-hledgerImport opts ch csvSrc journalOut = do-  case extractImportDirs csvSrc of-    Right importDirs -> hledgerImport' opts ch importDirs csvSrc journalOut-    Left errorMessage -> do-      errExit 1 ch errorMessage csvSrc--hledgerImport' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> ImportDirs -> TurtlePath -> TurtlePath -> IO TurtlePath-hledgerImport' opts ch importDirs csvSrc journalOut = do-  let candidates = rulesFileCandidates csvSrc importDirs-  maybeRulesFile <- firstExistingFile candidates-  let relCSV = relativeToBase opts csvSrc-  case maybeRulesFile of-    Just rf -> do-      let relRules = relativeToBase opts rf-      let hledger = Turtle.format Turtle.fp $ FlowTypes.hlPath . hledgerInfo $ opts :: T.Text-      let args = ["print", "--rules-file", Turtle.format Turtle.fp rf, "--file", Turtle.format Turtle.fp csvSrc, "--output-file", Turtle.format Turtle.fp journalOut]-      let cmdLabel = Turtle.format ("importing '"%Turtle.fp%"' using rules file '"%Turtle.fp%"'") relCSV relRules-      _ <- timeAndExitOnErr opts ch cmdLabel channelOut channelErr (parAwareProc opts) (hledger, args, Turtle.empty)-      return journalOut-    Nothing ->-      do-        let relativeCandidates = map (relativeToBase opts) candidates-        let candidatesTxt = T.intercalate "\n" $ map (Turtle.format Turtle.fp) relativeCandidates-        let msg = Turtle.format ("I couldn't find an hledger rules file while trying to import\n"%Turtle.fp-                          %"\n\nI will happily use the first rules file I can find from any one of these "%Turtle.d%" files:\n"%Turtle.s-                          %"\n\nHere is a bit of documentation about rules files that you may find helpful:\n"%Turtle.s)-                  relCSV (length candidates) candidatesTxt (docURL "rules-files")-        errExit 1 ch msg csvSrc--rulesFileCandidates :: TurtlePath -> ImportDirs -> [TurtlePath]-rulesFileCandidates csvSrc importDirs = statementSpecificRulesFiles csvSrc importDirs ++ generalRulesFiles importDirs--importDirLines :: (ImportDirs -> TurtlePath) -> ImportDirs -> [Turtle.Line]-importDirLines dirFun importDirs = NonEmpty.toList $ Turtle.textToLines $ Turtle.format Turtle.fp $ Turtle.dirname $ dirFun importDirs--importDirLine :: (ImportDirs -> TurtlePath) -> ImportDirs -> Turtle.Line-importDirLine dirFun importDirs = foldl (<>) "" $ importDirLines dirFun importDirs--generalRulesFiles :: ImportDirs -> [TurtlePath]-generalRulesFiles importDirs = do-  let bank = importDirLines bankDir importDirs-  let account = importDirLines accountDir importDirs-  let accountRulesFile = accountDir importDirs </> buildFilename (bank ++ account) "rules"--  let bankRulesFile = importDir importDirs </> buildFilename bank "rules"-  [accountRulesFile, bankRulesFile]--statementSpecificRulesFiles :: TurtlePath -> ImportDirs -> [TurtlePath]-statementSpecificRulesFiles csvSrc importDirs = do-  let srcSuffix = snd $ T.breakOnEnd "_" (Turtle.format Turtle.fp (Turtle.basename csvSrc))--  if ((T.take 3 srcSuffix) == "rfo")-    then-    do-      let srcSpecificFilename = Turtle.fromText srcSuffix <.> "rules"-      map (</> srcSpecificFilename) [accountDir importDirs, bankDir importDirs, importDir importDirs]-    else []--customConstruct :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> TurtlePath -> TurtlePath -> IO TurtlePath-customConstruct opts ch constructScript bank account owner csvSrc journalOut = do-  let script = Turtle.format Turtle.fp constructScript :: T.Text-  let relScript = relativeToBase opts constructScript-  let constructArgs = [Turtle.format Turtle.fp csvSrc, "-", Turtle.lineToText bank, Turtle.lineToText account, Turtle.lineToText owner]-  let constructCmdText = Turtle.format ("Running: "%Turtle.fp%" "%Turtle.s) relScript (showCmdArgs constructArgs)-  let stdLines = inprocWithErrFun (channelErrLn ch) (script, constructArgs, Turtle.empty)-  let hledger = Turtle.format Turtle.fp $ FlowTypes.hlPath . hledgerInfo $ opts :: T.Text-  let args = ["print", "--ignore-assertions", "--file", "-", "--output-file", Turtle.format Turtle.fp journalOut]-  let relSrc = relativeToBase opts csvSrc-  let cmdLabel = Turtle.format ("executing '"%Turtle.fp%"' on '"%Turtle.fp%"'") relScript relSrc-  _ <- timeAndExitOnErr' opts ch cmdLabel [constructCmdText] channelOut channelErr (parAwareProc opts) (hledger, args, stdLines)-  return journalOut
src/Hledger/Flow/Common.hs view
@@ -2,7 +2,7 @@  module Hledger.Flow.Common where -import qualified Turtle as Turtle+import qualified Turtle import Turtle ((%), (</>), (<.>))  import Prelude hiding (putStrLn)@@ -17,7 +17,6 @@  import qualified Control.Foldl as Fold import qualified Data.Map.Strict as Map-import Data.Time.LocalTime  import Data.Function (on) import qualified Data.List as List (nub, null, sort, sortBy, groupBy)@@ -26,6 +25,7 @@ import Hledger.Flow.Types import qualified Hledger.Flow.Import.Types as IT +import Hledger.Flow.Logging import Hledger.Flow.PathHelpers (TurtlePath) import Hledger.Flow.BaseDir (turtleBaseDir, relativeToBase, relativeToBase') import Hledger.Flow.DocHelpers (docURL)@@ -86,22 +86,7 @@ showCmdArgs args = T.intercalate " " (map escapeArg args)  escapeArg :: T.Text -> T.Text-escapeArg a = if (T.count " " a > 0) then "'" <> a <> "'" else a--dummyLogger :: TChan LogMessage -> T.Text -> IO ()-dummyLogger _ _ = return ()--channelOut :: TChan LogMessage -> T.Text -> IO ()-channelOut ch txt = atomically $ writeTChan ch $ StdOut txt--channelOutLn :: TChan LogMessage -> T.Text -> IO ()-channelOutLn ch txt = channelOut ch (txt <> "\n")--channelErr :: TChan LogMessage -> T.Text -> IO ()-channelErr ch txt = atomically $ writeTChan ch $ StdErr txt--channelErrLn :: TChan LogMessage -> T.Text -> IO ()-channelErrLn ch txt = channelErr ch (txt <> "\n")+escapeArg a = if T.count " " a > 0 then "'" <> a <> "'" else a  errExit :: Int -> TChan LogMessage -> T.Text -> a -> IO a errExit exitStatus ch = errExit' exitStatus (channelErrLn ch)@@ -113,34 +98,6 @@   _ <- Turtle.exit $ Turtle.ExitFailure exitStatus   return dummyReturnValue -timestampPrefix :: T.Text -> IO T.Text-timestampPrefix txt = do-  t <- getZonedTime-  return $ Turtle.format (Turtle.s%"\thledger-flow "%Turtle.s) (Turtle.repr t) txt--logToChannel :: TChan LogMessage -> T.Text -> IO ()-logToChannel ch msg = do-  ts <- timestampPrefix msg-  channelErrLn ch ts--consoleChannelLoop :: TChan LogMessage -> IO ()-consoleChannelLoop ch = do-  logMsg <- atomically $ readTChan ch-  case logMsg of-    StdOut msg -> do-      T.hPutStr H.stdout msg-      consoleChannelLoop ch-    StdErr msg -> do-      T.hPutStr H.stderr msg-      consoleChannelLoop ch-    Terminate  -> return ()--terminateChannelLoop :: TChan LogMessage -> IO ()-terminateChannelLoop ch = atomically $ writeTChan ch Terminate--logVerbose :: HasVerbosity o => o -> TChan LogMessage -> T.Text -> IO ()-logVerbose opts ch msg = if (verbose opts) then logToChannel ch msg else return ()- descriptiveOutput :: T.Text -> T.Text -> T.Text descriptiveOutput outputLabel outTxt = do   if not (T.null outTxt)@@ -419,7 +376,7 @@  changeOutputPath :: TurtlePath -> TurtlePath -> TurtlePath changeOutputPath newOutputLocation srcFile = mconcat $ map changeSrcDir $ Turtle.splitDirectories srcFile-  where changeSrcDir file = if (file == "1-in/" || file == "2-preprocessed/") then newOutputLocation else file+  where changeSrcDir file = if file == "1-in/" || file == "2-preprocessed/" then newOutputLocation else file  importDirBreakdown ::  TurtlePath -> [TurtlePath] importDirBreakdown = importDirBreakdown' []@@ -427,7 +384,7 @@ importDirBreakdown' :: [TurtlePath] -> TurtlePath -> [TurtlePath] importDirBreakdown' acc path = do   let dir = Turtle.directory path-  if (Turtle.dirname dir == "import" || (Turtle.dirname dir == ""))+  if Turtle.dirname dir == "import" || (Turtle.dirname dir == "")     then dir:acc     else importDirBreakdown' (dir:acc) $ Turtle.parent dir 
+ src/Hledger/Flow/DateTime.hs view
@@ -0,0 +1,12 @@+module Hledger.Flow.DateTime where++import Data.Time.Clock+import Data.Time.Calendar++currentDate :: IO (Integer,Int,Int) -- :: (year,month,day)+currentDate = toGregorian . utctDay <$> getCurrentTime++currentYear :: IO Integer+currentYear = do+  (y, _, _) <- currentDate+  return y
+ src/Hledger/Flow/Import/CSVImport.hs view
@@ -0,0 +1,212 @@+{-# LANGUAGE OverloadedStrings #-}++module Hledger.Flow.Import.CSVImport+    ( importCSVs+    ) where++import qualified Turtle hiding (stdout, stderr, proc, procStrictWithErr)+import Turtle ((%), (</>), (<.>))+import Prelude hiding (putStrLn, take)+import qualified Data.Text as T+import qualified Data.List.NonEmpty as NonEmpty+import qualified Hledger.Flow.Types as FlowTypes+import Hledger.Flow.Import.Types+import Hledger.Flow.BaseDir (relativeToBase, effectiveRunDir)+import Hledger.Flow.Import.ImportHelpers+import Hledger.Flow.PathHelpers (TurtlePath, pathToTurtle)+import Hledger.Flow.DocHelpers (docURL)+import Hledger.Flow.Common+import Hledger.Flow.Logging+import Hledger.Flow.RuntimeOptions+import Control.Concurrent.STM+import Control.Monad+import Data.Maybe (fromMaybe, isNothing)++type FileWasGenerated = Bool++importCSVs :: RuntimeOptions -> IO ()+importCSVs opts = Turtle.sh (+  do+    ch <- Turtle.liftIO newTChanIO+    logHandle <- Turtle.fork $ consoleChannelLoop ch+    Turtle.liftIO $ when (showOptions opts) (channelOutLn ch (Turtle.repr opts))+    Turtle.liftIO $ logVerbose opts ch "Starting import"+    (journals, diff) <- Turtle.time $ Turtle.liftIO $ importCSVs' opts ch+    let generatedJournals = filter snd journals+    Turtle.liftIO $ channelOutLn ch $ Turtle.format ("Imported "%Turtle.d%"/"%Turtle.d%" journals in "%Turtle.s) (length generatedJournals) (length journals) $ Turtle.repr diff+    Turtle.liftIO $ terminateChannelLoop ch+    Turtle.wait logHandle+  )++importCSVs' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> IO [(TurtlePath, FileWasGenerated)]+importCSVs' opts ch = do+  let effectiveDir = effectiveRunDir (baseDir opts) (importRunDir opts)+  let startYearMsg = maybe " " (Turtle.format (" (for the year " % Turtle.d % " and onwards) ")) (importStartYear opts)+  channelOutLn ch $ Turtle.format ("Collecting input files"%Turtle.s%"from "%Turtle.fp) startYearMsg (pathToTurtle effectiveDir)+  (inputFiles, diff) <- Turtle.time $ findInputFiles (fromMaybe 0 $ importStartYear opts) effectiveDir++  let fileCount = length inputFiles+  if fileCount == 0 && isNothing (importStartYear opts) then+    do+      let msg = Turtle.format ("I couldn't find any input files underneath "%Turtle.fp+                        %"\n\nhledger-flow expects to find its input files in specifically\nnamed directories.\n\n"%+                        "Have a look at the documentation for a detailed explanation:\n"%Turtle.s) (pathToTurtle effectiveDir) (docURL "input-files")+      errExit 1 ch msg []+    else+    do+      channelOutLn ch $ Turtle.format ("Found "%Turtle.d%" input files"%Turtle.s%"in "%Turtle.s%". Proceeding with import...") fileCount startYearMsg (Turtle.repr diff)+      let actions = map (extractAndImport opts ch . pathToTurtle) inputFiles :: [IO (TurtlePath, FileWasGenerated)]+      importedJournals <- parAwareActions opts actions+      (journalsOnDisk, journalFindTime) <- Turtle.time $ findJournalFiles effectiveDir+      (_, writeIncludeTime1) <- Turtle.time $ writeIncludesUpTo opts ch (pathToTurtle effectiveDir) $ fmap pathToTurtle journalsOnDisk+      (_, writeIncludeTime2) <- Turtle.time $ writeToplevelAllYearsInclude opts+      let includeGenTime = journalFindTime + writeIncludeTime1 + writeIncludeTime2+      channelOutLn ch $ Turtle.format ("Wrote include files for "%Turtle.d%" journals in "%Turtle.s) (length journalsOnDisk) (Turtle.repr includeGenTime)+      return importedJournals++extractAndImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> IO (TurtlePath, FileWasGenerated)+extractAndImport opts ch inputFile = do+  case extractImportDirs inputFile of+    Right importDirs -> importCSV opts ch importDirs inputFile+    Left errorMessage -> do+      errExit 1 ch errorMessage (inputFile, False)++importCSV :: RuntimeOptions -> TChan FlowTypes.LogMessage -> ImportDirs -> TurtlePath -> IO (TurtlePath, FileWasGenerated)+importCSV opts ch importDirs srcFile = do+  let preprocessScript = accountDir importDirs </> "preprocess"+  let constructScript = accountDir importDirs </> "construct"+  let bankName = importDirLine bankDir importDirs+  let accountName = importDirLine accountDir importDirs+  let ownerName = importDirLine ownerDir importDirs+  (csvFile, preprocessHappened) <- preprocessIfNeeded opts ch preprocessScript bankName accountName ownerName srcFile+  let journalOut = changePathAndExtension "3-journal" "journal" csvFile+  shouldImport <- if onlyNewFiles opts && not preprocessHappened+    then not <$> verboseTestFile opts ch journalOut+    else return True++  importFun <- if shouldImport+    then constructOrImport opts ch constructScript bankName accountName ownerName+    else do+      _ <- logNewFileSkip opts ch "import" journalOut+      return $ \_p1 _p2 -> return journalOut+  Turtle.mktree $ Turtle.directory journalOut+  out <- importFun csvFile journalOut+  return (out, shouldImport)++constructOrImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> IO (TurtlePath -> TurtlePath -> IO TurtlePath)+constructOrImport opts ch constructScript bankName accountName ownerName = do+  constructScriptExists <- verboseTestFile opts ch constructScript+  if constructScriptExists+    then return $ customConstruct opts ch constructScript bankName accountName ownerName+    else return $ hledgerImport opts ch++preprocessIfNeeded :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> TurtlePath -> IO (TurtlePath, Bool)+preprocessIfNeeded opts ch script bank account owner src = do+  let csvOut = changePathAndExtension "2-preprocessed" "csv" src+  scriptExists <- verboseTestFile opts ch script+  shouldProceed <- if onlyNewFiles opts +    then do+      targetExists <- verboseTestFile opts ch csvOut+      return $ scriptExists && not targetExists+    else return scriptExists+  if shouldProceed+    then do+     out <- preprocess opts ch script bank account owner src csvOut+     return (out, True)+    else do+      _ <- logNewFileSkip opts ch "preprocess" csvOut+      return (src, False)++logNewFileSkip :: RuntimeOptions -> TChan FlowTypes.LogMessage -> T.Text -> TurtlePath -> IO ()+logNewFileSkip opts ch logIdentifier absTarget =+  Control.Monad.when (onlyNewFiles opts) $ do+   let relativeTarget = relativeToBase opts absTarget+   logVerbose opts ch+     $ Turtle.format+        ("Skipping " % Turtle.s+         % " - only creating new files and this output file already exists: '"+         % Turtle.fp+         % "'") logIdentifier relativeTarget++preprocess :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> TurtlePath -> TurtlePath -> IO TurtlePath+preprocess opts ch script bank account owner src csvOut = do+  Turtle.mktree $ Turtle.directory csvOut+  let args = [Turtle.format Turtle.fp src, Turtle.format Turtle.fp csvOut, Turtle.lineToText bank, Turtle.lineToText account, Turtle.lineToText owner]+  let relScript = relativeToBase opts script+  let relSrc = relativeToBase opts src+  let cmdLabel = Turtle.format ("executing '"%Turtle.fp%"' on '"%Turtle.fp%"'") relScript relSrc+  _ <- timeAndExitOnErr opts ch cmdLabel channelOut channelErr (parAwareProc opts) (Turtle.format Turtle.fp script, args, Turtle.empty)+  return csvOut++hledgerImport :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> TurtlePath -> IO TurtlePath+hledgerImport opts ch csvSrc journalOut = do+  case extractImportDirs csvSrc of+    Right importDirs -> hledgerImport' opts ch importDirs csvSrc journalOut+    Left errorMessage -> do+      errExit 1 ch errorMessage csvSrc++hledgerImport' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> ImportDirs -> TurtlePath -> TurtlePath -> IO TurtlePath+hledgerImport' opts ch importDirs csvSrc journalOut = do+  let candidates = rulesFileCandidates csvSrc importDirs+  maybeRulesFile <- firstExistingFile candidates+  let relCSV = relativeToBase opts csvSrc+  case maybeRulesFile of+    Just rf -> do+      let relRules = relativeToBase opts rf+      let hledger = Turtle.format Turtle.fp $ FlowTypes.hlPath . hledgerInfo $ opts :: T.Text+      let args = ["print", "--rules-file", Turtle.format Turtle.fp rf, "--file", Turtle.format Turtle.fp csvSrc, "--output-file", Turtle.format Turtle.fp journalOut]+      let cmdLabel = Turtle.format ("importing '"%Turtle.fp%"' using rules file '"%Turtle.fp%"'") relCSV relRules+      _ <- timeAndExitOnErr opts ch cmdLabel channelOut channelErr (parAwareProc opts) (hledger, args, Turtle.empty)+      return journalOut+    Nothing ->+      do+        let relativeCandidates = map (relativeToBase opts) candidates+        let candidatesTxt = T.intercalate "\n" $ map (Turtle.format Turtle.fp) relativeCandidates+        let msg = Turtle.format ("I couldn't find an hledger rules file while trying to import\n"%Turtle.fp+                          %"\n\nI will happily use the first rules file I can find from any one of these "%Turtle.d%" files:\n"%Turtle.s+                          %"\n\nHere is a bit of documentation about rules files that you may find helpful:\n"%Turtle.s)+                  relCSV (length candidates) candidatesTxt (docURL "rules-files")+        errExit 1 ch msg csvSrc++rulesFileCandidates :: TurtlePath -> ImportDirs -> [TurtlePath]+rulesFileCandidates csvSrc importDirs = statementSpecificRulesFiles csvSrc importDirs ++ generalRulesFiles importDirs++importDirLines :: (ImportDirs -> TurtlePath) -> ImportDirs -> [Turtle.Line]+importDirLines dirFun importDirs = NonEmpty.toList $ Turtle.textToLines $ Turtle.format Turtle.fp $ Turtle.dirname $ dirFun importDirs++importDirLine :: (ImportDirs -> TurtlePath) -> ImportDirs -> Turtle.Line+importDirLine dirFun importDirs = foldl (<>) "" $ importDirLines dirFun importDirs++generalRulesFiles :: ImportDirs -> [TurtlePath]+generalRulesFiles importDirs = do+  let bank = importDirLines bankDir importDirs+  let account = importDirLines accountDir importDirs+  let accountRulesFile = accountDir importDirs </> buildFilename (bank ++ account) "rules"++  let bankRulesFile = importDir importDirs </> buildFilename bank "rules"+  [accountRulesFile, bankRulesFile]++statementSpecificRulesFiles :: TurtlePath -> ImportDirs -> [TurtlePath]+statementSpecificRulesFiles csvSrc importDirs = do+  let srcSuffix = snd $ T.breakOnEnd "_" (Turtle.format Turtle.fp (Turtle.basename csvSrc))++  if ((T.take 3 srcSuffix) == "rfo")+    then+    do+      let srcSpecificFilename = Turtle.fromText srcSuffix <.> "rules"+      map (</> srcSpecificFilename) [accountDir importDirs, bankDir importDirs, importDir importDirs]+    else []++customConstruct :: RuntimeOptions -> TChan FlowTypes.LogMessage -> TurtlePath -> Turtle.Line -> Turtle.Line -> Turtle.Line -> TurtlePath -> TurtlePath -> IO TurtlePath+customConstruct opts ch constructScript bank account owner csvSrc journalOut = do+  let script = Turtle.format Turtle.fp constructScript :: T.Text+  let relScript = relativeToBase opts constructScript+  let constructArgs = [Turtle.format Turtle.fp csvSrc, "-", Turtle.lineToText bank, Turtle.lineToText account, Turtle.lineToText owner]+  let constructCmdText = Turtle.format ("Running: "%Turtle.fp%" "%Turtle.s) relScript (showCmdArgs constructArgs)+  let stdLines = inprocWithErrFun (channelErrLn ch) (script, constructArgs, Turtle.empty)+  let hledger = Turtle.format Turtle.fp $ FlowTypes.hlPath . hledgerInfo $ opts :: T.Text+  let args = ["print", "--ignore-assertions", "--file", "-", "--output-file", Turtle.format Turtle.fp journalOut]+  let relSrc = relativeToBase opts csvSrc+  let cmdLabel = Turtle.format ("executing '"%Turtle.fp%"' on '"%Turtle.fp%"'") relScript relSrc+  _ <- timeAndExitOnErr' opts ch cmdLabel [constructCmdText] channelOut channelErr (parAwareProc opts) (hledger, args, stdLines)+  return journalOut
+ src/Hledger/Flow/Import/ImportHelpers.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE QuasiQuotes #-}++module Hledger.Flow.Import.ImportHelpers (findInputFiles, findJournalFiles) where++import Path+import Data.Char (isDigit)++import Hledger.Flow.PathHelpers (AbsDir, AbsFile, RelDir, findFilesIn)++findInputFiles :: Integer -> AbsDir -> IO [AbsFile]+findInputFiles startYear = do+  let excludeDirs = [[Path.reldir|2-preprocessed|], [Path.reldir|3-journal|]] ++ commonExcludeDirs+  findFilesIn (includeYearFilesForParent [Path.reldir|1-in|] startYear) excludeDirs++findJournalFiles :: AbsDir -> IO [AbsFile]+findJournalFiles = do+  let excludeDirs = [[Path.reldir|1-in|], [Path.reldir|2-preprocessed|]] ++ commonExcludeDirs+  findFilesIn (includeYearFilesForParent [Path.reldir|3-journal|] 0) excludeDirs++-- | Include only files directly underneath parentDir/yearDir, e.g. 1-in/2020/* or 3-journal/2020/*+includeYearFilesForParent :: RelDir -> Integer -> AbsDir -> Bool+includeYearFilesForParent parentDir startYear d = (dirname . parent) d == parentDir+  && length shortDirName == 4+  && all isDigit shortDirName+  && read shortDirName >= startYear+    where shortDirName = dirToStringNoSlash d++dirToStringNoSlash :: AbsDir -> String+dirToStringNoSlash = init . Path.toFilePath . Path.dirname++commonExcludeDirs :: [RelDir]+commonExcludeDirs = [[Path.reldir|_manual_|], [Path.reldir|__pycache__|]]
+ src/Hledger/Flow/Logging.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE OverloadedStrings #-}++module Hledger.Flow.Logging where++import Hledger.Flow.Types+import Control.Concurrent.STM+import Control.Monad (when)++import qualified Data.Text as T+import qualified Data.Text.IO as T+import Data.Time.LocalTime (getZonedTime)++import qualified GHC.IO.Handle.FD as H+import qualified Turtle+import Turtle ((%))++dummyLogger :: TChan LogMessage -> T.Text -> IO ()+dummyLogger _ _ = return ()++channelOut :: TChan LogMessage -> T.Text -> IO ()+channelOut ch txt = atomically $ writeTChan ch $ StdOut txt++channelOutLn :: TChan LogMessage -> T.Text -> IO ()+channelOutLn ch txt = channelOut ch (txt <> "\n")++channelErr :: TChan LogMessage -> T.Text -> IO ()+channelErr ch txt = atomically $ writeTChan ch $ StdErr txt++channelErrLn :: TChan LogMessage -> T.Text -> IO ()+channelErrLn ch txt = channelErr ch (txt <> "\n")++logToChannel :: TChan LogMessage -> T.Text -> IO ()+logToChannel ch msg = do+  ts <- timestampPrefix msg+  channelErrLn ch ts++timestampPrefix :: T.Text -> IO T.Text+timestampPrefix txt = do+  t <- getZonedTime+  return $ Turtle.format (Turtle.s%"\thledger-flow "%Turtle.s) (Turtle.repr t) txt++consoleChannelLoop :: TChan LogMessage -> IO ()+consoleChannelLoop ch = do+  logMsg <- atomically $ readTChan ch+  case logMsg of+    StdOut msg -> do+      T.hPutStr H.stdout msg+      consoleChannelLoop ch+    StdErr msg -> do+      T.hPutStr H.stderr msg+      consoleChannelLoop ch+    Terminate  -> return ()++terminateChannelLoop :: TChan LogMessage -> IO ()+terminateChannelLoop ch = atomically $ writeTChan ch Terminate++logVerbose :: HasVerbosity o => o -> TChan LogMessage -> T.Text -> IO ()+logVerbose opts ch msg = when (verbose opts) $ logToChannel ch msg
src/Hledger/Flow/PathHelpers.hs view
@@ -6,6 +6,8 @@ import Control.Monad.IO.Class (MonadIO)  import qualified Data.Text as T++import Path ((</>)) import qualified Path import qualified Path.IO as Path import qualified Turtle@@ -61,3 +63,20 @@  pathSize' :: Path.Path b Path.Dir -> Int -> Int pathSize' p count = if Path.parent p == p then count else pathSize' (Path.parent p) (count+1)++-- | Do a recursive search starting from the given directory.+-- Return all files contained in each directory which matches the given predicate.+findFilesIn :: MonadIO m+  => (AbsDir -> Bool) -- ^ Do we want the files in this directory?+  -> [RelDir]         -- ^ Exclude these directory names+  -> AbsDir           -- ^ Top of the search tree+  -> m [AbsFile]      -- ^ Absolute paths to all files in the directories which match the predicate+findFilesIn includePred excludeDirs = Path.walkDirAccum (Just excludeHandler) accumulator+  where excludeHandler currentDir _ _ = return $ Path.WalkExclude (map (currentDir </>) excludeDirs)+        accumulator currentDir _ files =+          if includePred currentDir+              then return $ excludeHiddenFiles files+              else return []++excludeHiddenFiles :: [AbsFile] -> [AbsFile]+excludeHiddenFiles = filter (\ f -> head (Path.toFilePath (Path.filename f)) /= '.')
src/Hledger/Flow/Reports.hs view
@@ -19,6 +19,7 @@ import qualified Data.Text as T import qualified Hledger.Flow.Types as FlowTypes import Hledger.Flow.PathHelpers (TurtlePath)+import Hledger.Flow.Logging import qualified Data.List as List  data ReportParams = ReportParams { ledgerFile :: TurtlePath
src/Hledger/Flow/RuntimeOptions.hs view
@@ -7,6 +7,7 @@  data RuntimeOptions = RuntimeOptions { baseDir :: BaseDir                                      , importRunDir :: RunDir+                                     , importStartYear :: Maybe Integer                                      , onlyNewFiles :: Bool                                      , hfVersion :: T.Text                                      , hledgerInfo :: HledgerInfo@@ -18,13 +19,13 @@   deriving (Show)  instance HasVerbosity RuntimeOptions where-  verbose (RuntimeOptions _ _ _ _ _ _ v _ _) = v+  verbose (RuntimeOptions _ _ _ _ _ _ _ v _ _) = v  instance HasSequential RuntimeOptions where-  sequential (RuntimeOptions _ _ _ _ _ _ _ _ sq) = sq+  sequential (RuntimeOptions _ _ _ _ _ _ _ _ _ sq) = sq  instance HasBaseDir RuntimeOptions where-  baseDir (RuntimeOptions bd _ _ _ _ _ _ _ _) = bd+  baseDir (RuntimeOptions bd _ _ _ _ _ _ _ _ _) = bd  instance HasRunDir RuntimeOptions where-  importRunDir (RuntimeOptions _ rd _ _ _ _ _ _ _) = rd+  importRunDir (RuntimeOptions _ rd _ _ _ _ _ _ _ _) = rd
src/Hledger/Flow/Types.hs view
@@ -1,10 +1,9 @@ {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE FlexibleInstances #-} -module Hledger.Flow.Types-where+module Hledger.Flow.Types where -import qualified Turtle as Turtle (ExitCode, NominalDiffTime, Shell, Line)+import qualified Turtle (ExitCode, NominalDiffTime, Shell, Line) import qualified Data.Text as T import Data.Version 
test/TestHelpers.hs view
@@ -64,6 +64,7 @@ defaultOpts bd = RuntimeOptions {     baseDir = bd   , importRunDir = [reldir|./|]+  , importStartYear = Nothing   , onlyNewFiles = False   , hfVersion = versionInfo'   , hledgerInfo = defaultHlInfo