mirror of
https://github.com/cachix/stamina.hs.git
synced 2024-08-16 22:10:34 +03:00
implement Stamina
This commit is contained in:
parent
ad977f7248
commit
9b8c8229af
78
.github/workflows/ci.yml
vendored
78
.github/workflows/ci.yml
vendored
@ -1,16 +1,72 @@
|
|||||||
name: "CI"
|
name: "build & test"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
pull_request:
|
||||||
- main
|
branches: [main]
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
build:
|
||||||
runs-on: ubuntu-latest
|
name: GHC ${{ matrix.ghc-version }} on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
ghc-version: ['9.8', '9.6', '9.4', '9.2']
|
||||||
|
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
ghc-version: '9.8'
|
||||||
|
- os: macos-latest
|
||||||
|
ghc-version: '9.8'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: freckle/stack-action@v4
|
|
||||||
|
- name: Set up GHC ${{ matrix.ghc-version }}
|
||||||
|
uses: haskell-actions/setup@v2
|
||||||
|
id: setup
|
||||||
|
with:
|
||||||
|
ghc-version: ${{ matrix.ghc-version }}
|
||||||
|
# Defaults, added for clarity:
|
||||||
|
cabal-version: 'latest'
|
||||||
|
cabal-update: true
|
||||||
|
|
||||||
|
- name: Configure the build
|
||||||
|
run: |
|
||||||
|
cabal configure --enable-tests --enable-benchmarks --disable-documentation
|
||||||
|
cabal build all --dry-run
|
||||||
|
# The last step generates dist-newstyle/cache/plan.json for the cache key.
|
||||||
|
|
||||||
|
- name: Restore cached dependencies
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
id: cache
|
||||||
|
env:
|
||||||
|
key: ${{ runner.os }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}
|
||||||
|
with:
|
||||||
|
path: ${{ steps.setup.outputs.cabal-store }}
|
||||||
|
key: ${{ env.key }}-plan-${{ hashFiles('**/plan.json') }}
|
||||||
|
restore-keys: ${{ env.key }}-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
# If we had an exact cache hit, the dependencies will be up to date.
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: cabal build all --only-dependencies
|
||||||
|
|
||||||
|
# Cache dependencies already here, so that we do not have to rebuild them should the subsequent steps fail.
|
||||||
|
- name: Save cached dependencies
|
||||||
|
uses: actions/cache/save@v3
|
||||||
|
# If we had an exact cache hit, trying to save the cache would error because of key clash.
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
with:
|
||||||
|
path: ${{ steps.setup.outputs.cabal-store }}
|
||||||
|
key: ${{ steps.cache.outputs.cache-primary-key }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cabal build all
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cabal test all
|
||||||
|
|
||||||
|
- name: Build documentation
|
||||||
|
run: cabal haddock all
|
||||||
|
1
README.lhs
Symbolic link
1
README.lhs
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
README.md
|
75
README.md
75
README.md
@ -7,57 +7,46 @@ A retry Haskell library for humans:
|
|||||||
- **Exponential backoff** with **jitter** between retries.
|
- **Exponential backoff** with **jitter** between retries.
|
||||||
- Limit the **attempts** of retries and **total** time.
|
- Limit the **attempts** of retries and **total** time.
|
||||||
- `Stamina.HTTP` for retrying retriable `Network.HTTP.Client` exceptions.
|
- `Stamina.HTTP` for retrying retriable `Network.HTTP.Client` exceptions.
|
||||||
- Introspectable retry state for logging using `RetryStatus`.
|
- Introspectable retry state for logging using `RetryStatus`, including the exception that occoured.
|
||||||
- Support resetting the retry state when the action is long-running and an attempt works.
|
- Support resetting the retry state when the action is long-running and an attempt works.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
```haskell
|
- `RetryAction`
|
||||||
import Control.Exception (Exception, Handler)
|
- `RetryStatus`
|
||||||
import Control.Monad.IO.Class (MonadIO)
|
- `defaults`
|
||||||
import Data.Time.Clock (DiffTime)
|
- `retry`
|
||||||
|
- `retryOnExceptions`
|
||||||
defaults :: RetrySettings
|
|
||||||
|
|
||||||
data RetryStatus = RetryStatus
|
|
||||||
{ attempts :: Int,
|
|
||||||
delay :: DiffTime,
|
|
||||||
totalDelay :: DiffTime,
|
|
||||||
reset :: IO ()
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Retry on all sync exceptions
|
|
||||||
retry :: MonadIO m
|
|
||||||
=> RetrySettings
|
|
||||||
-> (RetryStatus -> m a)
|
|
||||||
-> m a
|
|
||||||
|
|
||||||
-- Retry on specific exceptions
|
|
||||||
retryOnExceptions :: (Exception e, MonadIO m)
|
|
||||||
=> RetrySettings
|
|
||||||
-> [Handler RetryAction]
|
|
||||||
-> (RetryStatus -> m a)
|
|
||||||
-> m a
|
|
||||||
|
|
||||||
data RetryAction =
|
|
||||||
Skip -- Propagate the exception.
|
|
||||||
| Retry -- Retry with the delay according to the settings.
|
|
||||||
| RetryDelay DiffTime -- Retry after the given delay.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
```haskell
|
```haskell
|
||||||
|
|
||||||
import qualified Stamina
|
import qualified Stamina
|
||||||
|
import Control.Monad.Catch (throwM)
|
||||||
|
|
||||||
main :: IO ()
|
go :: IO ()
|
||||||
main = do
|
go = do
|
||||||
Stamina.retry Stamina.defaults $ \retryStatus -> do
|
defaults <- Stamina.defaults
|
||||||
... monadic logic that raises exceptions
|
Stamina.retry defaults $ \retryStatus -> do
|
||||||
|
throwM $ userError "nope"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Example to catch specific exceptions
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
|
||||||
|
isDoesNotExistError :: IOError -> Stamina.RetryAction
|
||||||
|
isDoesNotExistError _ = Stamina.Retry
|
||||||
|
|
||||||
|
go2 :: IO ()
|
||||||
|
go2 = do
|
||||||
|
defaults <- Stamina.defaults
|
||||||
|
Stamina.retryOnExceptions defaults isDoesNotExistError $ \retryStatus -> do
|
||||||
|
throwM $ userError "nope"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
1. Install [devenv.sh](https://devenv.sh/getting-started/).
|
1. Install [devenv.sh](https://devenv.sh/getting-started/).
|
||||||
@ -70,3 +59,11 @@ main = do
|
|||||||
|
|
||||||
- Heavily inspired by [stamina for Python](https://stamina.hynek.me/en/stable/tutorial.html#retries).
|
- Heavily inspired by [stamina for Python](https://stamina.hynek.me/en/stable/tutorial.html#retries).
|
||||||
- [retry](https://github.com/Soostone/retry) as case study for what needs to be supported.
|
- [retry](https://github.com/Soostone/retry) as case study for what needs to be supported.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Test setup</summary>
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
main = undefined
|
||||||
|
```
|
||||||
|
</details>
|
148
src/Stamina.hs
148
src/Stamina.hs
@ -1,60 +1,71 @@
|
|||||||
module Stamina
|
module Stamina
|
||||||
( retry,
|
( -- functions
|
||||||
|
retry,
|
||||||
retryOnExceptions,
|
retryOnExceptions,
|
||||||
|
-- types
|
||||||
RetrySettings (..),
|
RetrySettings (..),
|
||||||
defaults,
|
defaults,
|
||||||
RetryAction (..),
|
RetryAction (..),
|
||||||
RetryStatus (..),
|
RetryStatus (..),
|
||||||
|
-- raising exceptions
|
||||||
|
escalateWith,
|
||||||
|
escalate,
|
||||||
|
withLeft,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.Exception (Exception, Handler)
|
import Control.Concurrent (newMVar, putMVar)
|
||||||
import Control.Monad.IO.Class (MonadIO)
|
import Control.Exception (Exception (..), SomeAsyncException (SomeAsyncException), SomeException, throwIO)
|
||||||
import Control.Retry qualified as Retry
|
import Control.Monad (void)
|
||||||
import Data.Time.Clock (DiffTime)
|
import Control.Monad.Catch (MonadCatch, throwM, try)
|
||||||
|
import Control.Monad.IO.Class (MonadIO, liftIO)
|
||||||
|
import Data.Maybe (isJust)
|
||||||
|
import Data.Time.Clock (DiffTime, secondsToDiffTime)
|
||||||
|
import System.Random (randomRIO)
|
||||||
|
|
||||||
|
-- | Settings for the retry functions.
|
||||||
data RetrySettings = RetrySettings
|
data RetrySettings = RetrySettings
|
||||||
{ initialRetryStatus :: RetryStatus, -- Initial status of the retry, useful to override when resuming a retry
|
{ initialRetryStatus :: RetryStatus, -- Initial status of the retry, useful to override when resuming a retry
|
||||||
maxAttempts :: Int, -- Maximum number of attempts. Can be combined with a timeout. Default to 10.
|
maxAttempts :: Maybe Int, -- Maximum number of attempts. Can be combined with a timeout. Default to 10.
|
||||||
maxTime :: DiffTime, -- Maximum time for all retries. Can be combined with attempts. Default to 45.0.
|
maxTime :: Maybe DiffTime, -- Maximum time for all retries. Can be combined with attempts. Default to 60s.
|
||||||
backoffInitialRetryDelay :: DiffTime, -- Minimum backoff before the first retry. Default to 0.1.
|
backoffMaxRetryDelay :: DiffTime, -- Maximum backoff between retries at any time. Default to 5s.
|
||||||
backoffMaxRetryDelay :: DiffTime, -- Maximum backoff between retries at any time. Default to 5.0.
|
backoffJitter :: Double, -- Maximum jitter that is added to retry back-off delays (the actual jitter added is a random number between 0 and backoffJitter). Defaults to 1.0.
|
||||||
backoffJitter :: Double, -- Maximum jitter that is added to retry back-off delays (the actual jitter added is a random number between 0 and backoffJitter). Default to 1.0.
|
|
||||||
backoffExpBase :: Double -- The exponential base used to compute the retry backoff. Defaults to 2.0.
|
backoffExpBase :: Double -- The exponential base used to compute the retry backoff. Defaults to 2.0.
|
||||||
}
|
}
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
-- Tracks the status of a retry
|
-- | Tracks the status of a retry
|
||||||
|
--
|
||||||
-- All fields will be zero if no retries have been attempted yet.
|
-- All fields will be zero if no retries have been attempted yet.
|
||||||
data RetryStatus = RetryStatus
|
data RetryStatus = RetryStatus
|
||||||
{ attempts :: Int, -- Number of retry attempts so far.
|
{ attempts :: Int, -- Number of retry attempts so far.
|
||||||
delay :: DiffTime, -- Delay before the next retry.
|
delay :: DiffTime, -- Delay before the next retry.
|
||||||
totalDelay :: DiffTime, -- Total delay so far.
|
totalDelay :: DiffTime, -- Total delay so far.
|
||||||
reset :: IO () -- Reset the retry status to the initial state.
|
reset :: IO (), -- Reset the retry status to the initial state.
|
||||||
|
lastException :: Maybe SomeException -- The last exception that was thrown.
|
||||||
}
|
}
|
||||||
deriving (Show)
|
|
||||||
|
|
||||||
defaults :: RetrySettings
|
defaults :: IO RetrySettings
|
||||||
defaults =
|
defaults = do
|
||||||
-- TODO: Implement reset
|
resetMVar <- newMVar ()
|
||||||
RetrySettings
|
return $
|
||||||
{ initialRetryStatus =
|
RetrySettings
|
||||||
RetryStatus
|
{ initialRetryStatus =
|
||||||
{ attempts = 0,
|
RetryStatus
|
||||||
delay = 0,
|
{ attempts = 0,
|
||||||
totalDelay = 0,
|
delay = 0,
|
||||||
reset = pure ()
|
totalDelay = 0,
|
||||||
},
|
reset = void $ putMVar resetMVar (),
|
||||||
maxAttempts = 10,
|
lastException = Nothing
|
||||||
maxTime = 45.0,
|
},
|
||||||
backoffInitialRetryDelay = 0.1,
|
maxAttempts = Just 10,
|
||||||
backoffMaxRetryDelay = 5.0,
|
maxTime = Just $ secondsToDiffTime 60,
|
||||||
backoffJitter = 1.0,
|
backoffMaxRetryDelay = 5.0,
|
||||||
backoffExpBase = 2.0
|
backoffJitter = 1.0,
|
||||||
}
|
backoffExpBase = 2.0
|
||||||
|
}
|
||||||
|
|
||||||
data RetryAction
|
data RetryAction
|
||||||
= Skip -- Propagate the exception.
|
= RaiseException -- Propagate the exception.
|
||||||
| Retry -- Retry with the delay according to the settings.
|
| Retry -- Retry with the delay according to the settings.
|
||||||
| RetryDelay DiffTime -- Retry after the given delay.
|
| RetryDelay DiffTime -- Retry after the given delay.
|
||||||
|
|
||||||
@ -68,9 +79,70 @@ data RetryAction
|
|||||||
-- Since x**0 is always 1, the first backoff is within the interval [backoff_initial,backoff_initial+backoff_jitter]. Thus, with default values between 0.1 and 1.1 seconds.
|
-- Since x**0 is always 1, the first backoff is within the interval [backoff_initial,backoff_initial+backoff_jitter]. Thus, with default values between 0.1 and 1.1 seconds.
|
||||||
|
|
||||||
-- If all retries fail, the last exception is let through.
|
-- If all retries fail, the last exception is let through.
|
||||||
retry :: MonadIO m => RetrySettings -> (RetryStatus -> m a) -> m a
|
retry :: (MonadCatch m, MonadIO m) => RetrySettings -> (RetryStatus -> m a) -> m a
|
||||||
retry settings action = undefined
|
retry settings = retryOnExceptions settings skipAsyncExceptions
|
||||||
|
where
|
||||||
|
skipAsyncExceptions :: SomeException -> RetryAction
|
||||||
|
skipAsyncExceptions exc = case fromException exc of
|
||||||
|
Just (SomeAsyncException _) -> RaiseException
|
||||||
|
Nothing -> Retry
|
||||||
|
|
||||||
-- Same as retry, but only retry on the given exceptions.
|
-- TODO: implement reset
|
||||||
retryOnExceptions :: MonadIO m => RetrySettings -> [Handler RetryAction] -> (RetryStatus -> m a) -> m a
|
-- Same as retry, but only retry the given exceptions.
|
||||||
retryOnExceptions settings handlers action = undefined
|
retryOnExceptions ::
|
||||||
|
(Exception exc, MonadIO m, MonadCatch m) =>
|
||||||
|
RetrySettings ->
|
||||||
|
(exc -> RetryAction) ->
|
||||||
|
(RetryStatus -> m a) ->
|
||||||
|
m a
|
||||||
|
retryOnExceptions settings handler action =
|
||||||
|
go $ initialRetryStatus settings
|
||||||
|
where
|
||||||
|
-- go :: (MonadCatch m, MonadIO m) => RetryStatus -> m a
|
||||||
|
go retryStatus = do
|
||||||
|
result <- try $ action retryStatus
|
||||||
|
case result of
|
||||||
|
Right out -> return out
|
||||||
|
Left exception -> case handler exception of
|
||||||
|
RaiseException -> throwM exception
|
||||||
|
Retry -> do
|
||||||
|
delay_ <- liftIO $ increaseDelay retryStatus
|
||||||
|
maybeAttempt exception retryStatus delay_
|
||||||
|
RetryDelay delay_ -> do
|
||||||
|
maybeAttempt exception retryStatus delay_
|
||||||
|
|
||||||
|
updateRetryStatus :: RetryStatus -> DiffTime -> SomeException -> RetryStatus
|
||||||
|
updateRetryStatus status delay_ exception =
|
||||||
|
status
|
||||||
|
{ attempts = attempts status + 1,
|
||||||
|
delay = delay_,
|
||||||
|
totalDelay = totalDelay status + delay_,
|
||||||
|
lastException = Just exception
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseDelay :: MonadIO m => RetryStatus -> m DiffTime
|
||||||
|
increaseDelay retryStatus = do
|
||||||
|
let RetryStatus {attempts} = retryStatus
|
||||||
|
let RetrySettings {backoffMaxRetryDelay, backoffJitter, backoffExpBase} = settings
|
||||||
|
jitter <- randomRIO (0, backoffJitter)
|
||||||
|
return $ min backoffMaxRetryDelay $ secondsToDiffTime $ floor $ backoffExpBase ** (fromIntegral attempts - 1) + jitter
|
||||||
|
|
||||||
|
-- maybeAttempt :: (Exception exc, MonadCatch m, MonadIO m) => exc -> RetryStatus -> DiffTime -> m a
|
||||||
|
maybeAttempt exception retryStatus delay_ = do
|
||||||
|
let RetrySettings {maxTime, maxAttempts} = settings
|
||||||
|
if (isJust maxTime && Just (totalDelay retryStatus + delay_) > maxTime)
|
||||||
|
|| (isJust maxAttempts && Just (attempts retryStatus + 1) > maxAttempts)
|
||||||
|
then throwM exception
|
||||||
|
else go $ updateRetryStatus retryStatus delay_ $ toException exception
|
||||||
|
|
||||||
|
-- | Escalate an Either to an exception by converting the Left value to an exception.
|
||||||
|
escalateWith :: (Exception exc) => (err -> exc) -> Either err a -> IO a
|
||||||
|
escalateWith f = either (throwIO . f) return
|
||||||
|
|
||||||
|
-- | Convert a Maybe to an Either.
|
||||||
|
withLeft :: a -> Maybe b -> Either a b
|
||||||
|
withLeft a = maybe (Left a) Right
|
||||||
|
|
||||||
|
-- | Escalate an Either to an exception.
|
||||||
|
escalate :: (Exception exc) => Either exc a -> IO a
|
||||||
|
escalate = escalateWith id
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
module Stamina.HTTP (retry) where
|
module Stamina.HTTP (retry) where
|
||||||
|
|
||||||
import Control.Exception (Handler)
|
import Control.Exception (SomeException)
|
||||||
|
import Control.Monad.Catch (MonadCatch)
|
||||||
import Control.Monad.IO.Class (MonadIO)
|
import Control.Monad.IO.Class (MonadIO)
|
||||||
import Network.HTTP.Client qualified as HTTP
|
import Network.HTTP.Client qualified as HTTP
|
||||||
import Stamina qualified
|
import Stamina qualified
|
||||||
|
|
||||||
handlers :: [Handler Stamina.RetryAction]
|
handler :: SomeException -> Stamina.RetryAction
|
||||||
handlers = undefined
|
handler = undefined
|
||||||
|
|
||||||
retry :: MonadIO m => Stamina.RetrySettings -> (Stamina.RetryStatus -> m a) -> m a
|
retry :: (MonadIO m, MonadCatch m) => Stamina.RetrySettings -> (Stamina.RetryStatus -> m a) -> m a
|
||||||
retry settings action = Stamina.retryOnExceptions settings handlers action
|
retry settings = Stamina.retryOnExceptions settings handler
|
||||||
|
@ -1,109 +1,41 @@
|
|||||||
cabal-version: 3.4
|
cabal-version: 3.4
|
||||||
-- The cabal-version field refers to the version of the .cabal specification,
|
|
||||||
-- and can be different from the cabal-install (the tool) version and the
|
|
||||||
-- Cabal (the library) version you are using. As such, the Cabal (the library)
|
|
||||||
-- version used must be equal or greater than the version stated in this field.
|
|
||||||
-- Starting from the specification version 2.2, the cabal-version field must be
|
|
||||||
-- the first thing in the cabal file.
|
|
||||||
|
|
||||||
-- Initial package description 'stamina' generated by
|
|
||||||
-- 'cabal init'. For further documentation, see:
|
|
||||||
-- http://haskell.org/cabal/users-guide/
|
|
||||||
--
|
|
||||||
-- The name of the package.
|
|
||||||
name: stamina
|
name: stamina
|
||||||
|
|
||||||
-- The package version.
|
|
||||||
-- See the Haskell package versioning policy (PVP) for standards
|
|
||||||
-- guiding when and how versions should be incremented.
|
|
||||||
-- https://pvp.haskell.org
|
|
||||||
-- PVP summary: +-+------- breaking API changes
|
|
||||||
-- | | +----- non-breaking API additions
|
|
||||||
-- | | | +--- code changes with no API change
|
|
||||||
version: 0.1.0.0
|
version: 0.1.0.0
|
||||||
|
|
||||||
-- A short (one-line) description of the package.
|
|
||||||
synopsis: Retries for humans
|
synopsis: Retries for humans
|
||||||
|
|
||||||
-- A longer description of the package.
|
|
||||||
-- description:
|
|
||||||
|
|
||||||
-- URL for the project homepage or repository.
|
|
||||||
homepage: https://github.com/cachix/stamina.hs
|
homepage: https://github.com/cachix/stamina.hs
|
||||||
|
|
||||||
-- The license under which the package is released.
|
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
|
|
||||||
-- The file containing the license text.
|
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
|
|
||||||
-- The package author(s).
|
|
||||||
author: Domen Kožar
|
author: Domen Kožar
|
||||||
|
|
||||||
-- An email address to which users can send suggestions, bug reports, and patches.
|
|
||||||
maintainer: domen@cachix.org
|
maintainer: domen@cachix.org
|
||||||
|
|
||||||
-- A copyright notice.
|
|
||||||
-- copyright:
|
|
||||||
category: Control
|
category: Control
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
|
|
||||||
-- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.
|
|
||||||
extra-doc-files: CHANGELOG.md
|
extra-doc-files: CHANGELOG.md
|
||||||
|
|
||||||
-- Extra source files to be distributed with the package, such as examples, or a tutorial module.
|
|
||||||
-- extra-source-files:
|
|
||||||
|
|
||||||
common warnings
|
common warnings
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
|
default-language: GHC2021
|
||||||
|
|
||||||
library
|
library
|
||||||
-- Import common warning flags.
|
|
||||||
import: warnings
|
import: warnings
|
||||||
|
|
||||||
-- Modules exported by the library.
|
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
Stamina
|
Stamina
|
||||||
, Stamina.HTTP
|
, Stamina.HTTP
|
||||||
|
build-depends: base, http-client, time, exceptions, random
|
||||||
-- Modules included in this library but not exported.
|
|
||||||
-- other-modules:
|
|
||||||
|
|
||||||
-- LANGUAGE extensions used by modules in this package.
|
|
||||||
-- other-extensions:
|
|
||||||
|
|
||||||
-- Other library packages from which modules are imported.
|
|
||||||
build-depends: base, http-client, time, retry
|
|
||||||
|
|
||||||
-- Directories containing source files.
|
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
|
|
||||||
-- Base language which the package is written in.
|
|
||||||
default-language: GHC2021
|
|
||||||
|
|
||||||
test-suite stamina-test
|
test-suite stamina-test
|
||||||
-- Import common warning flags.
|
|
||||||
import: warnings
|
import: warnings
|
||||||
|
|
||||||
-- Base language which the package is written in.
|
|
||||||
default-language: GHC2021
|
default-language: GHC2021
|
||||||
|
|
||||||
-- Modules included in this executable, other than Main.
|
|
||||||
-- other-modules:
|
|
||||||
|
|
||||||
-- LANGUAGE extensions used by modules in this package.
|
|
||||||
-- other-extensions:
|
|
||||||
|
|
||||||
-- The interface type and version of the test suite.
|
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
|
|
||||||
-- Directories containing source files.
|
|
||||||
hs-source-dirs: test
|
hs-source-dirs: test
|
||||||
|
|
||||||
-- The entrypoint to the test suite.
|
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
|
|
||||||
-- Test dependencies.
|
|
||||||
build-depends:
|
build-depends:
|
||||||
base,
|
base,
|
||||||
stamina
|
stamina
|
||||||
|
|
||||||
|
-- typechecks README during CI but doesn't run it
|
||||||
|
executable readme
|
||||||
|
main-is: README.lhs
|
||||||
|
build-depends: stamina, base, exceptions
|
||||||
|
ghc-options: -pgmL markdown-unlit
|
||||||
|
build-tool-depends: markdown-unlit:markdown-unlit
|
||||||
|
Loading…
Reference in New Issue
Block a user