{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Backend.Python.Validator (run, PythonValidator (..), PythonValidatorEnv (..)) where
import Control.Monad.Reader
import Convex.Validator
import System.Directory (createDirectoryIfMissing, doesFileExist)
import System.Exit (ExitCode (..))
import System.FilePath ((</>))
import System.IO (hGetContents)
import System.Process (CreateProcess (..), StdStream (..), createProcess, proc, waitForProcess)
newtype PythonValidator a = PythonValidator
{ forall a. PythonValidator a -> ReaderT PythonValidatorEnv IO a
runPythonValidator :: ReaderT PythonValidatorEnv IO a
}
deriving ((forall a b. (a -> b) -> PythonValidator a -> PythonValidator b)
-> (forall a b. a -> PythonValidator b -> PythonValidator a)
-> Functor PythonValidator
forall a b. a -> PythonValidator b -> PythonValidator a
forall a b. (a -> b) -> PythonValidator a -> PythonValidator b
forall (f :: * -> *).
(forall a b. (a -> b) -> f a -> f b)
-> (forall a b. a -> f b -> f a) -> Functor f
$cfmap :: forall a b. (a -> b) -> PythonValidator a -> PythonValidator b
fmap :: forall a b. (a -> b) -> PythonValidator a -> PythonValidator b
$c<$ :: forall a b. a -> PythonValidator b -> PythonValidator a
<$ :: forall a b. a -> PythonValidator b -> PythonValidator a
Functor, Functor PythonValidator
Functor PythonValidator =>
(forall a. a -> PythonValidator a)
-> (forall a b.
PythonValidator (a -> b) -> PythonValidator a -> PythonValidator b)
-> (forall a b c.
(a -> b -> c)
-> PythonValidator a -> PythonValidator b -> PythonValidator c)
-> (forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b)
-> (forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator a)
-> Applicative PythonValidator
forall a. a -> PythonValidator a
forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator a
forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
forall a b.
PythonValidator (a -> b) -> PythonValidator a -> PythonValidator b
forall a b c.
(a -> b -> c)
-> PythonValidator a -> PythonValidator b -> PythonValidator c
forall (f :: * -> *).
Functor f =>
(forall a. a -> f a)
-> (forall a b. f (a -> b) -> f a -> f b)
-> (forall a b c. (a -> b -> c) -> f a -> f b -> f c)
-> (forall a b. f a -> f b -> f b)
-> (forall a b. f a -> f b -> f a)
-> Applicative f
$cpure :: forall a. a -> PythonValidator a
pure :: forall a. a -> PythonValidator a
$c<*> :: forall a b.
PythonValidator (a -> b) -> PythonValidator a -> PythonValidator b
<*> :: forall a b.
PythonValidator (a -> b) -> PythonValidator a -> PythonValidator b
$cliftA2 :: forall a b c.
(a -> b -> c)
-> PythonValidator a -> PythonValidator b -> PythonValidator c
liftA2 :: forall a b c.
(a -> b -> c)
-> PythonValidator a -> PythonValidator b -> PythonValidator c
$c*> :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
*> :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
$c<* :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator a
<* :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator a
Applicative, Applicative PythonValidator
Applicative PythonValidator =>
(forall a b.
PythonValidator a -> (a -> PythonValidator b) -> PythonValidator b)
-> (forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b)
-> (forall a. a -> PythonValidator a)
-> Monad PythonValidator
forall a. a -> PythonValidator a
forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
forall a b.
PythonValidator a -> (a -> PythonValidator b) -> PythonValidator b
forall (m :: * -> *).
Applicative m =>
(forall a b. m a -> (a -> m b) -> m b)
-> (forall a b. m a -> m b -> m b)
-> (forall a. a -> m a)
-> Monad m
$c>>= :: forall a b.
PythonValidator a -> (a -> PythonValidator b) -> PythonValidator b
>>= :: forall a b.
PythonValidator a -> (a -> PythonValidator b) -> PythonValidator b
$c>> :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
>> :: forall a b.
PythonValidator a -> PythonValidator b -> PythonValidator b
$creturn :: forall a. a -> PythonValidator a
return :: forall a. a -> PythonValidator a
Monad, Monad PythonValidator
Monad PythonValidator =>
(forall a. IO a -> PythonValidator a) -> MonadIO PythonValidator
forall a. IO a -> PythonValidator a
forall (m :: * -> *).
Monad m =>
(forall a. IO a -> m a) -> MonadIO m
$cliftIO :: forall a. IO a -> PythonValidator a
liftIO :: forall a. IO a -> PythonValidator a
MonadIO, MonadReader PythonValidatorEnv, Monad PythonValidator
Monad PythonValidator =>
(forall a. String -> PythonValidator a)
-> MonadFail PythonValidator
forall a. String -> PythonValidator a
forall (m :: * -> *).
Monad m =>
(forall a. String -> m a) -> MonadFail m
$cfail :: forall a. String -> PythonValidator a
fail :: forall a. String -> PythonValidator a
MonadFail)
data PythonValidatorEnv = PythonValidatorEnv {PythonValidatorEnv -> String
projectPath :: FilePath}
run :: PythonValidatorEnv -> PythonValidator a -> IO a
run :: forall a. PythonValidatorEnv -> PythonValidator a -> IO a
run PythonValidatorEnv
renv PythonValidator a
action = ReaderT PythonValidatorEnv IO a -> PythonValidatorEnv -> IO a
forall r (m :: * -> *) a. ReaderT r m a -> r -> m a
runReaderT (PythonValidator a -> ReaderT PythonValidatorEnv IO a
forall a. PythonValidator a -> ReaderT PythonValidatorEnv IO a
runPythonValidator PythonValidator a
action) PythonValidatorEnv
renv
instance Validator PythonValidator where
setup :: PythonValidator ()
setup = do
PythonValidatorEnv
config <- PythonValidator PythonValidatorEnv
forall r (m :: * -> *). MonadReader r m => m r
ask
let pythonProjectPath :: String
pythonProjectPath = PythonValidatorEnv -> String
projectPath PythonValidatorEnv
config
let stubsPath :: String
stubsPath = String
pythonProjectPath String -> String -> String
</> String
"stubs"
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ Bool -> String -> IO ()
createDirectoryIfMissing Bool
True String
stubsPath
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$
String -> IO Bool
doesFileExist (String
pythonProjectPath String -> String -> String
</> String
"mypy.ini") IO Bool -> (Bool -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Bool
True -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
Bool
False -> String -> String -> IO ()
writeFile (String
pythonProjectPath String -> String -> String
</> String
"mypy.ini") String
mypyIniContent
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$
String -> IO Bool
doesFileExist (String
stubsPath String -> String -> String
</> String
"pydantic.pyi") IO Bool -> (Bool -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Bool
True -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
Bool
False -> String -> String -> IO ()
writeFile (String
stubsPath String -> String -> String
</> String
"pydantic.pyi") String
pydanticStubContent
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$
String -> IO Bool
doesFileExist (String
stubsPath String -> String -> String
</> String
"convex.pyi") IO Bool -> (Bool -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Bool
True -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
Bool
False -> String -> String -> IO ()
writeFile (String
stubsPath String -> String -> String
</> String
"convex.pyi") String
convexStubContent
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$
String -> IO Bool
doesFileExist (String
stubsPath String -> String -> String
</> String
"pydantic_core.pyi") IO Bool -> (Bool -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
Bool
True -> () -> IO ()
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
Bool
False -> String -> String -> IO ()
writeFile (String
stubsPath String -> String -> String
</> String
"pydantic_core.pyi") String
""
() -> PythonValidator ()
forall a. a -> PythonValidator a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
validate :: String -> PythonValidator (Maybe String)
validate String
generatedCode = do
String
pythonProjectPath <- (PythonValidatorEnv -> String) -> PythonValidator String
forall r (m :: * -> *) a. MonadReader r m => (r -> a) -> m a
asks PythonValidatorEnv -> String
projectPath
let generatedFilePath :: String
generatedFilePath = String
pythonProjectPath String -> String -> String
</> String
"generated_api.py"
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ String -> String -> IO ()
writeFile String
generatedFilePath String
generatedCode
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ String -> IO ()
putStrLn String
"[Validator] Running 'mypy'..."
let mypyCmd :: CreateProcess
mypyCmd = (String -> [String] -> CreateProcess
proc String
"mypy" [String
"."]) {cwd = Just pythonProjectPath, std_out = CreatePipe, std_err = CreatePipe}
(Maybe Handle
_, Just Handle
hOut, Just Handle
hErr, ProcessHandle
handle) <- IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
-> PythonValidator
(Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
-> PythonValidator
(Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle))
-> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
-> PythonValidator
(Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
forall a b. (a -> b) -> a -> b
$ CreateProcess
-> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
createProcess CreateProcess
mypyCmd
ExitCode
exitCode <- IO ExitCode -> PythonValidator ExitCode
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO ExitCode -> PythonValidator ExitCode)
-> IO ExitCode -> PythonValidator ExitCode
forall a b. (a -> b) -> a -> b
$ ProcessHandle -> IO ExitCode
waitForProcess ProcessHandle
handle
if ExitCode
exitCode ExitCode -> ExitCode -> Bool
forall a. Eq a => a -> a -> Bool
== ExitCode
ExitSuccess
then do
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ String -> IO ()
putStrLn String
"[Validator] Validation successful."
String
content <- IO String -> PythonValidator String
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO String -> PythonValidator String)
-> IO String -> PythonValidator String
forall a b. (a -> b) -> a -> b
$ String -> IO String
readFile String
generatedFilePath
Maybe String -> PythonValidator (Maybe String)
forall a. a -> PythonValidator a
forall (m :: * -> *) a. Monad m => a -> m a
return (Maybe String -> PythonValidator (Maybe String))
-> Maybe String -> PythonValidator (Maybe String)
forall a b. (a -> b) -> a -> b
$ String -> Maybe String
forall a. a -> Maybe a
Just String
content
else do
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ String -> IO ()
putStrLn String
"[Validator] Error: 'mypy' failed. The generated code has type errors."
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ Handle -> IO String
hGetContents Handle
hOut IO String -> (String -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= String -> IO ()
putStrLn
IO () -> PythonValidator ()
forall a. IO a -> PythonValidator a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO () -> PythonValidator ()) -> IO () -> PythonValidator ()
forall a b. (a -> b) -> a -> b
$ Handle -> IO String
hGetContents Handle
hErr IO String -> (String -> IO ()) -> IO ()
forall a b. IO a -> (a -> IO b) -> IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= String -> IO ()
putStrLn
Maybe String -> PythonValidator (Maybe String)
forall a. a -> PythonValidator a
forall (m :: * -> *) a. Monad m => a -> m a
return Maybe String
forall a. Maybe a
Nothing
mypyIniContent :: String
mypyIniContent :: String
mypyIniContent =
[String] -> String
unlines
[ String
"[mypy]",
String
"python_version = 3.9",
String
"disallow_untyped_defs = True",
String
"check_untyped_defs = True",
String
"warn_return_any = True",
String
"ignore_missing_imports = True"
]
pydanticStubContent :: String
pydanticStubContent :: String
pydanticStubContent =
[String] -> String
unlines
[ String
"from typing import Any, Type",
String
"",
String
"class BaseModel:",
String
" def model_validate(cls: Type, obj: Any) -> Any: ...",
String
" def model_dump_json(self, *, indent: int | None = ...) -> str: ...",
String
"",
String
"def Field(default: Any = ..., *, alias: str | None = ...) -> Any: ...",
String
"",
String
"class TypeAdapter:",
String
" def __init__(self, type: Any) -> None: ...",
String
" def validate_python(self, data: Any) -> Any: ...",
String
"",
String
"class ValidationError(Exception): ..."
]
convexStubContent :: String
convexStubContent :: String
convexStubContent =
[String] -> String
unlines
[ String
"from typing import Any, Iterator",
String
"",
String
"class ConvexClient:",
String
" def __init__(self, url: str) -> None: ...",
String
" def set_admin_auth(self, key: str) -> None: ...",
String
" def set_auth(self, token: str) -> None: ...",
String
" def query(self, path: str, args: dict[str, Any]) -> Any: ...",
String
" def mutation(self, path: str, args: dict[str, Any]) -> Any: ...",
String
" def action(self, path: str, args: dict[str, Any]) -> Any: ...",
String
" def subscribe(self, path: str, args: dict[str, Any]) -> Iterator[Any]: ...",
String
"",
String
"class ConvexError(Exception): ..."
]