packages feed

advent-of-code-api 0.2.10.0 → 0.2.11.0

raw patch · 5 files changed

+145/−9 lines, 5 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ Advent: DayStats :: Integer -> Integer -> DayStats
+ Advent: [AoCStats] :: AoC (Map Day DayStats)
+ Advent: [_aInferRankOnSubmission] :: AoCOpts -> Bool
+ Advent: [dsGold] :: DayStats -> Integer
+ Advent: [dsSilver] :: DayStats -> Integer
+ Advent: data DayStats
+ Advent: inferSubmitRes :: AoCOpts -> Day -> Part -> SubmitRes -> IO (Either AoCError SubmitRes)
+ Advent: inferSubmitRes_ :: AoCOpts -> Day -> Part -> SubmitRes -> IO SubmitRes
+ Advent: statsForDayPart :: Day -> Part -> Stats -> Integer
+ Advent: type Stats = Map Day DayStats
+ Advent.API: instance Advent.API.FromTags "pre" Advent.Types.Stats
+ Advent.Types: DayStats :: Integer -> Integer -> DayStats
+ Advent.Types: [dsGold] :: DayStats -> Integer
+ Advent.Types: [dsSilver] :: DayStats -> Integer
+ Advent.Types: data DayStats
+ Advent.Types: instance GHC.Classes.Eq Advent.Types.DayStats
+ Advent.Types: instance GHC.Classes.Ord Advent.Types.DayStats
+ Advent.Types: instance GHC.Generics.Generic Advent.Types.DayStats
+ Advent.Types: instance GHC.Read.Read Advent.Types.DayStats
+ Advent.Types: instance GHC.Show.Show Advent.Types.DayStats
+ Advent.Types: type Stats = Map Day DayStats
- Advent: AoCOpts :: String -> Integer -> AoCUserAgent -> Maybe FilePath -> Bool -> Int -> AoCOpts
+ Advent: AoCOpts :: String -> Integer -> AoCUserAgent -> Maybe FilePath -> Bool -> Int -> Bool -> AoCOpts
- Advent.API: adventAPIClient :: Maybe AoCUserAgent -> Integer -> ClientM NextDayTime :<|> ((Day -> ClientM (Map Part Text) :<|> (ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)))) :<|> (ClientM GlobalLeaderboard :<|> ((Day -> ClientM DailyLeaderboard) :<|> (PublicCode -> ClientM Leaderboard))))
+ Advent.API: adventAPIClient :: Maybe AoCUserAgent -> Integer -> ClientM NextDayTime :<|> (ClientM Stats :<|> ((Day -> ClientM (Map Part Text) :<|> (ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)))) :<|> (ClientM GlobalLeaderboard :<|> ((Day -> ClientM DailyLeaderboard) :<|> (PublicCode -> ClientM Leaderboard)))))
- Advent.API: type AdventAPI = Header "User-Agent" AoCUserAgent :> Capture "year" Integer :> Get '[Scripts] NextDayTime :<|> "day" :> Capture "day" Day :> Get '[Articles] Map Part Text :<|> "input" :> Get '[RawText] Text :<|> "answer" :> ReqBody '[FormUrlEncoded] SubmitInfo :> Post '[Articles] Text :<|> SubmitRes :<|> "leaderboard" :> Get '[Divs] GlobalLeaderboard :<|> "day" :> Capture "day" Day :> Get '[Divs] DailyLeaderboard :<|> "private" :> "view" :> Capture "code" PublicCode :> Get '[JSON] Leaderboard
+ Advent.API: type AdventAPI = Header "User-Agent" AoCUserAgent :> Capture "year" Integer :> Get '[Scripts] NextDayTime :<|> "stats" :> Get '[Pres] Stats :<|> "day" :> Capture "day" Day :> Get '[Articles] Map Part Text :<|> "input" :> Get '[RawText] Text :<|> "answer" :> ReqBody '[FormUrlEncoded] SubmitInfo :> Post '[Articles] Text :<|> SubmitRes :<|> "leaderboard" :> Get '[Divs] GlobalLeaderboard :<|> "day" :> Capture "day" Day :> Get '[Divs] DailyLeaderboard :<|> "private" :> "view" :> Capture "code" PublicCode :> Get '[JSON] Leaderboard

Files

CHANGELOG.md view
@@ -1,8 +1,22 @@ Changelog ========= +Version 0.2.11.0+----------------++*December 6, 2025*++<https://github.com/mstksg/advent-of-code-api/releases/tag/v0.2.11.0>++*   Add endpoint for `<year>/stats` to get current star counts. Use with care,+    never cached.+*   Add `inferSubmitRes`, `inferSubmitRes_` to add stats-based rank at time of+    execution.+*   Add `_aInferRankOnSubmission` (default `False`) to automatically tag your+    `SubmitRes` with an inferred rank based on stats.+ Version 0.2.10.0----------------+----------------  *December 1, 2025* 
advent-of-code-api.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           advent-of-code-api-version:        0.2.10.0+version:        0.2.11.0 synopsis:       Advent of Code REST API bindings and servant API description:    Haskell bindings for Advent of Code REST API and a servant API.  Please use                 responsibly! See README.md or "Advent" module for an introduction and
src/Advent.hs view
@@ -52,9 +52,14 @@   , Part(..)   , Day(..)   , NextDayTime(..)+  , DayStats(..)+  , Stats   , AoCOpts(..)   , AoCUserAgent(..)   , SubmitRes(..), showSubmitRes+  , statsForDayPart+  , inferSubmitRes+  , inferSubmitRes_   , runAoC   , runAoC_   , defaultAoCOpts@@ -243,6 +248,15 @@     AoCNextDayTime         :: AoC NextDayTime +    -- | Fetch the per-day completion stats for the current year.  Does not+    -- require a session key.+    --+    -- /Cacheing rules/: Not cached.+    --+    -- @since 0.2.11.0+    AoCStats+        :: AoC Stats+ deriving instance Show (AoC a) deriving instance Typeable (AoC a) @@ -255,6 +269,7 @@ aocDay (AoCDailyLeaderboard d) = Just d aocDay AoCGlobalLeaderboard = Nothing aocDay AoCNextDayTime       = Nothing+aocDay AoCStats             = Nothing  -- | A possible (syncronous, logical, pure) error returnable from 'runAoC'. -- Does not cover any asynchronous or IO errors.@@ -306,6 +321,10 @@       -- | Throttle delay, in milliseconds.  Minimum is 1000000.  Default       -- is 3000000 (3 seconds).     , _aThrottle   :: Int+      -- | Attempt to infer submission rank by fetching stats after a+      -- correct submission that doesn't include rank information.+      -- Default is False.+    , _aInferRankOnSubmission :: Bool     }   deriving (Show, Typeable, Generic) @@ -326,6 +345,7 @@     , _aCache      = Nothing     , _aForce      = False     , _aThrottle   = 3000000+    , _aInferRankOnSubmission = False     }  -- | HTTPS base of Advent of Code API.@@ -339,14 +359,16 @@     AoCInput  i       -> let _ :<|> r :<|> _ = adventAPIPuzzleClient aua yr i in r     AoCSubmit i p ans -> let _ :<|> _ :<|> r = adventAPIPuzzleClient aua yr i                          in  r (SubmitInfo p ans) <&> \(x :<|> y) -> (x, y)-    AoCLeaderboard c  -> let _ :<|> _ :<|> _ :<|> _ :<|> r = adventAPIClient aua yr+    AoCLeaderboard c  -> let _ :<|> _ :<|> _ :<|> _ :<|> _ :<|> r = adventAPIClient aua yr                          in  r (PublicCode c)-    AoCDailyLeaderboard d -> let _ :<|> _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr+    AoCDailyLeaderboard d -> let _ :<|> _ :<|> _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr                              in  r d-    AoCGlobalLeaderboard  -> let _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr+    AoCGlobalLeaderboard  -> let _ :<|> _ :<|> _ :<|> r :<|> _ :<|> _ = adventAPIClient aua yr                              in  r-    AoCNextDayTime        -> let r :<|> _ = adventAPIClient aua yr+    AoCNextDayTime        -> let r :<|> _ :<|> _ :<|> _ :<|> _ :<|> _ = adventAPIClient aua yr                              in  r+    AoCStats              -> let _ :<|> r :<|> _ :<|> _ :<|> _ :<|> _ = adventAPIClient aua yr+                             in  r   -- | Cache file for a given 'AoC' command@@ -363,6 +385,7 @@     AoCDailyLeaderboard d  -> Just $ printf "daily/%04d/%02d.json" yr (dayInt d)     AoCGlobalLeaderboard{} -> Just $ printf "global/%04d.json" yr     AoCNextDayTime         -> Nothing+    AoCStats               -> Nothing   where     keyDir = case sess of       Nothing -> ""@@ -375,7 +398,7 @@ -- of leading and trailing whitespace and run through 'URI.encode' -- before submitting. runAoC :: AoCOpts -> AoC a -> IO (Either AoCError a)-runAoC AoCOpts{..} a = do+runAoC opts@AoCOpts{..} a = do     (keyMayb, cacheDir) <- case _aCache of       Just c  -> pure (Nothing, c)       Nothing -> (Just _aSessionKey,) . (</> "advent-of-code-api") <$> getTemporaryDirectory@@ -406,7 +429,12 @@            . throttling aocThrottler (max 1000000 _aThrottle)            $ runClientM (aocReq (Just _aUserAgent) _aYear a) =<< aocClientEnv _aSessionKey       mcr <- maybe (throwError AoCThrottleError) pure mtr-      either (throwError . AoCClientError) pure mcr+      res <- either (throwError . AoCClientError) pure mcr+      case (a, _aInferRankOnSubmission, res) of+        (AoCSubmit d p _, True, (txt, sr)) -> do+          sr' <- liftIO $ inferSubmitRes_ opts d p sr+          pure (txt, sr')+        _ -> pure res  -- | A version of 'runAoC' that throws an IO exception (of type 'AoCError') -- upon failure, instead of an 'Either'.@@ -415,6 +443,48 @@ runAoC_ :: AoCOpts -> AoC a -> IO a runAoC_ o = either throwIO pure <=< runAoC o +-- | Extract the relevant completion count for a given part on a day's stats.+--+-- @since 0.2.11.0+statsForDayPart :: Day -> Part -> Stats -> Integer+statsForDayPart d Part1 = maybe 0 dsSilver . M.lookup d+statsForDayPart d Part2 = maybe 0 dsGold   . M.lookup d++-- | If an Advent of Code submission returns 'SubCorrect' without an attached+-- rank, attempt to infer the rank from 'AoCStats', assuming that it was just+-- submitted and received at this moment.+--+-- If the response is anything other than 'SubCorrect Nothing', it is returned+-- unchanged.+--+-- @since 0.2.11.0+inferSubmitRes+    :: AoCOpts+    -> Day+    -> Part+    -> SubmitRes+    -> IO (Either AoCError SubmitRes)+inferSubmitRes opts d p sr = case sr of+    SubCorrect Nothing -> do+        stats <- runAoC opts AoCStats+        pure $ stats >>= \st ->+          case M.lookup d st of+            Just _  -> Right . SubCorrect . Just $ statsForDayPart d p st+            Nothing -> Right $ SubCorrect Nothing+    _ -> pure $ Right sr++-- | Variant of 'inferSubmitRes' that returns the original response if stats+-- lookup fails.+--+-- @since 0.2.11.0+inferSubmitRes_+    :: AoCOpts+    -> Day+    -> Part+    -> SubmitRes+    -> IO SubmitRes+inferSubmitRes_ opts d p sr = either (const sr) id <$> inferSubmitRes opts d p sr+ aocClientEnv :: String -> IO ClientEnv aocClientEnv s = do     t <- getCurrentTime@@ -473,6 +543,7 @@             pure $ Right lb         }     AoCNextDayTime{} -> noCache+    AoCStats{} -> noCache   where     expectedParts :: Set Part     expectedParts
src/Advent/API.hs view
@@ -113,6 +113,9 @@ -- | Interpret a response as a list of HTML 'T.Text' found in @<script>@ tags. type Scripts = HTMLTags "script" +-- | Interpret a response as a list of HTML 'T.Text' found in @<pre>@ tags.+type Pres = HTMLTags "pre"+ -- | Class for interpreting a list of 'T.Text' in tags to some desired -- output. --@@ -229,6 +232,9 @@           where             t' = "var " <> t <> " = " +instance FromTags "pre" Stats where+    fromTags _ = listToMaybe . mapMaybe parseStats+ -- | A structured user agent, based on -- <https://www.reddit.com/r/adventofcode/comments/z9dhtd/please_include_your_contact_info_in_the_useragent/> data AoCUserAgent = AoCUserAgent@@ -247,6 +253,7 @@       Header "User-Agent" AoCUserAgent    :> Capture "year" Integer    :> (Get '[Scripts] NextDayTime+  :<|> "stats" :> Get '[Pres] Stats   :<|> "day" :> Capture "day" Day              :> (Get '[Articles] (Map Part Text)             :<|> "input" :> Get '[RawText] Text@@ -273,6 +280,7 @@     :: Maybe AoCUserAgent     -> Integer     -> ClientM NextDayTime+  :<|> ClientM Stats   :<|> (Day -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)) )   :<|> ClientM GlobalLeaderboard   :<|> (Day -> ClientM DailyLeaderboard)@@ -288,7 +296,34 @@     -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)) adventAPIPuzzleClient aua y = pis   where-    _ :<|> pis :<|> _ = adventAPIClient aua y+    _ :<|> _ :<|> pis :<|> _ = adventAPIClient aua y++parseStats :: Text -> Maybe Stats+parseStats = fmap M.fromList . traverse parseStatLine . filter asBranch . H.parseTree+  where+    asBranch TagBranch{} = True+    asBranch _           = False++parseStatLine :: TagTree Text -> Maybe (Day, DayStats)+parseStatLine (TagBranch _ _ cs) = do+    dayTxt <- listToMaybe [t | TagLeaf (H.TagText t) <- cs, not (T.null (T.strip t))]+    dy     <- mkDay =<< readMaybe (T.unpack (T.strip dayTxt))+    gold   <- findNum "stats-both" cs+    silver <- findNum "stats-firstonly" cs+    pure (dy, DayStats gold silver)+  where+    findNum cls = listToMaybe . mapMaybe (go cls)+    go cls (TagBranch "span" attr inner) = do+      guard $ ("class", cls) `elem` attr+      readMaybe . T.unpack . T.strip $ innerText inner+    go _ _ = Nothing+parseStatLine _ = Nothing++innerText :: [TagTree Text] -> Text+innerText = T.concat . mapMaybe go . H.universeTree+  where+    go (TagLeaf (H.TagText t)) = Just t+    go _                       = Nothing  userNameNaked :: [TagTree Text] -> Maybe Text userNameNaked = (listToMaybe .) . mapMaybe $ \x -> do
src/Advent/Types.hs view
@@ -43,6 +43,8 @@   , GlobalLeaderboard(..)   , GlobalLeaderboardMember(..)   , NextDayTime(..)+  , DayStats(..)+  , Stats   -- * Util   , mkDay, mkDay_, dayInt   , _DayInt, pattern DayInt@@ -259,6 +261,20 @@ data NextDayTime = NextDayTime Day Int                  | NoNextDayTime   deriving (Show, Eq, Ord, Typeable, Generic)++-- | Stats for a single day on the event stats page.+--+-- @since 0.2.11.0+data DayStats = DayStats+    { dsGold   :: Integer   -- ^ Users who completed both parts+    , dsSilver :: Integer   -- ^ Users who only completed the first part+    }+  deriving (Show, Read, Eq, Ord, Typeable, Generic)++-- | Stats for all days on the event stats page.+--+-- @since 0.2.11.0+type Stats = Map Day DayStats  instance ToHttpApiData Part where     toUrlPiece = T.pack . show . partInt