Package: yuno
yuno(originally:saki)An account and database management framework, completing NasseManipulate your databases as if you never leaved PythonGetting StartedThese instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system.PrerequisitesPythonYou will need Python 3 to use this module# vermin outputMinimumrequiredversions:3.8
Incompatibleversions:2According to Vermin (--backport typing), Python 3.8 is needed fortyping.LiteralMongoDBI haven't checked the minimum requirements for the MongoDB features used in this library yet but you should always use the latest versions for maximum security.Also this framework has been tested on MongoDBv4.4.5(locally), MongoDBv4.4.12(on Atlas) and the latest version of MongoDB on theCI tests.Always check if your Python version works withyunobefore using it in production.InstallingOption 1: From PyPIpipinstall--upgradeyunoOption 2: From Gitpipinstall--upgradegit+https://github.com/Animenosekai/yunoYou can check if you successfully installed it by printing out its version:$python-c"import yuno; print(yuno.__version__)"# output:yunov1.0PurposeThis framework aims to bring effortless database management and database schemas in Python, in order to make everything predictable and easy to debug.You will never need to learn and search for obscure MongoDB features again because everything is made to behave like normal Python objects.UsageRunning a MongoDB (mongod) process from PythonIf you have a MongoDB database already running (for example: using MongoDB Atlas, or on a separate server), you can skip this part.Starting a processTo start a process you will need to create a newMongoDBobject with the configurations (refer toConfiguration) and start it from there.ExamplefromyunoimportMongoDBserver=MongoDB()server.start()# this will launch MongoDB and wait for it to run...server.kill()# this will kill the MongoDB processThestartmethod can get extra parameters that will modify the way MongoDB is launched:executable: The path to the MongoDB executable.wait: When you have setforktoFalse, this is the number of seconds to wait for the process to start. (I was planning to use the output from the process to determine when it is listening for connections butmongoddoesn't seem to "flush" its output so it was just hanging waiting for new output to come until the process was killed)keep_alive: Whether to keep the process alive or not (fork will be enabled) after Python quits (it will not registerself.killat cleanup).ConfigurationMongoDB configuration is made easy with Yuno!You can just pass configuration values intoMongoDBto configure it!You can configure a lot of important values natively for now and you can add more obscure ones when launching the process.List of supported configuration valueshost: The host of the MongoDB server (net.bindIp)port: The port of the MongoDB server (net.port)db_path: The path of the MongoDB database (storage.dbPath)fork: Whether to fork the MongoDB process or not (processManagement.fork)log_config: The MongoDB logging configuration (systemLog)max_connections: The maximum number of connections allowed (net.maxIncomingConnections)json_validation: Whether to enable the JSON validation or not (net.wireObjectCheck)ipv6: Whether to enable IPv6 or not (net.ipv6)monitoring: Whether to enable the free MongoDB monitoring or not (cloud.monitoring.free.state)About LogConfigThe content of thelog_configparameter should be aLogConfigobject (or a dictionary containing data to instantiate aLogConfigclass).This parameter is a class on its own because of its complexity.Parametersverbosity: The verbosity level of the MongoDB logging systempath: The path of the MongoDB log file. It can also be TERMINAL or SYSLOG (the systemLog.destination value will be set automatically according to this value)append: Whether to append to the log file or nottimezone: The timezone of the timestamps formatdebug: Whether to enable the debug mode or not (this will enable max verbosity and--traceExceptions)ExamplefromyunoimportMongoDB,LogConfigfromyuno.launcherimportTimezoneserver=MongoDB(db_path="./db/test",fork=True,monitoring=False,log_config=LogConfig(verbosity=2,path="./logs/db/test.log",timezone=Timezone.UTC))server.start()Loading and Dumping the configurationYou can also load the config from a file or dump it into a file using the built in methods (available onLogConfigandMongoDB):to_cli_argswill dump the configuration to a list of CLI arguments to give to theMongoDB(mongod) processloadswill load the configuration from a YAML string or a dictionary of valuesloadwill load the configuration from a YAML filedumpswill dump the configuration to a YAML stringdumpwill dump the configuration to a YAML fileto_dict(ordict(configuration_object)) will dump the configuration to a dictionary (you have acamelCaseparameter to make the keys camelCased)This is especially useful when you already have a configuration file or want to share the configuration to another user not using Yuno.ExamplefromyunoimportMongoDBserver=MongoDB()# will load all of the default configurationsserver.load("./db/mongo.conf")# will reinitialize the object with the config file, keeping the default values for values that aren't specifiedserver.dump("./db/mongo_new.conf")# will dump the configurations to a new file (to share or use later)# will show the configurationprint("MongoDB configuration")print("-"*10)forkey,valinserver.to_dict(camelCase=True):print(key,"=",val)Connecting to a MongoDB processWether you use the built-inMongoDBobject to launch a MongoDB process or just host it on the cloud/on another server, you will need to connect to it usingYunoClient.Here are the parameters used to connect to a server:host: The host or list of hostnames to connect to. You can usehostto pass in a URI string or a MongoDB object (in which case you won't need to useport).port: The port to connect to.tz_aware: Whether to use timezone aware datetimes or not.connect: Whether to connect before making any operation to the server or not.kwargs: Options to pass to the PyMongo client. (you don't need to setkwargsmanually, just pass the parameters as normal parameters)Example# Using the "MongoDB" objectfromyunoimportMongoDBfromyunoimportYunoClientserver=MongoDB()server.start()client=YunoClient(server)# you are connecting to the server hereUsing the clientTo access databases from the client, all you need to do is access its name as an attribute or an item:client.database_name# orclient["database_name"]In both case, this will return the same YunoDatabase object.Theclient["database_name"]syntax is especially useful if you use a database with the name of a method overwritten by YunoClient (for examplewatch,server_info,address,close, etc.).You can find a list of overwritten attributes under the__overwritten__attribute.YunoClient can be used to establish the database schema.This basically means that you can already type hint databases by creating your own clients.fromyunoimportYunoClientfromyunoimportYunoDatabaseclassMyClient(YunoClient):production:YunoDatabase# you can here use your custom databases (refer to the "Using databases" section)scores:YunoDatabaseclient=MyClient("mongodb+srv://anise:password@yunotest.host.mongodb.net/production")client.production# will return the `production` database# YunoDatabase('production')This helps with establishing a schema and helps your code editor guide you when writing code, resulting in less time searching for types, available databases and looking back at your code.It has serveral other methods, which are picked and adapted from the original PyMongoMongoClient.Examplescloseis used to close the connection to the serverdatabase_namesis used to retrieve the list of databases created.server_infowill return some info about the current MongoDB server in a special object calledBuildInfo(yuno.client.BuildInfo).watchreturns a cursor to watch the cluster for changes and events.onwill register a callback function which will be called when the specified operation/event is performed on the server.You can also use pythonic syntax to make some operations:Dropping a databasedelclient.test# ordelclient["test"]# will drop (delete) the "test" databaseUsing databasesA "database" is what holds collections in MongoDB.To access collections from the client, all you need to do is access its name as an attribute or an item:database.collection_name# ordatabase["collection_name"]In both case, this will return the same YunoCollection object.Thedatabase["collection_name"]syntax is especially useful if you use a database with the name of a method overwritten by YunoDatabase (for examplewatch,on,command,aggregate, etc.).You can find a list of overwritten attributes under the__overwritten__attribute.YunoCollectionis the class returned byYunoDatabasewhen querying for one.# using the client variable defined beforeaccounts=production_database.accounts# this is YunoCollection objectYou can define your own databases to type hint them (for the same reasons as before)fromyunoimportYunoClient,YunoDatabase,YunoCollectionclassMyDatabase(YunoDatabase):accounts:YunoCollection# you will also be able to create your own collectionsclassMyClient(YunoClient):production:MyDatabaseclient=MyClient("mongodb+srv://anise:password@yunotest.host.mongodb.net/production")production_database=client.production# will return the custom MyDatabase object and code editors will help you write code# MyDatabase('production')production_database.accounts# this is a YunoCollection objectIt has serveral other methods, which are picked and adapted from the original PyMongoMongoClient.Examplesaggregateis used to perform an aggregation on the databasecreate_collectionis used to create a new collectionlist_collection_namesreturns a list of collections availablewatchreturns a cursor to watch the database for changes and events.onwill register a callback function which will be called when the specified operation/event is performed on the database.You can also use pythonic syntax to make some operations:Dropping a collectiondeldatabase.account# ordeldatabase["account"]# will drop (delete) the "account" collectionUsing collectionsA "collection" is what holds documents in MongoDB.To access documents from the collection, all you need to do is access its name as an attribute or an item:collection.document_id# orcollection["document_id"]In both case, this will return the same YunoCollection object.Thecollection["document_id"]syntax is especially useful if you use a document with the _id of a method overwritten by YunoCollection (for examplewatch,on,find,aggregate, etc.).You can find a list of overwritten attributes under the__overwritten__attribute.YunoCollectionis the class returned byYunoDatabasewhen querying for one.# using the client variable defined beforeaccounts=production_database.accounts# this is YunoCollection objectYou can define your own collections to type hint them (for the same reasons as before)frombsonimportObjectIdfromyunoimportYunoClient,YunoDatabase,YunoCollection,YunoDictclassMyCollection(YunoCollection):special_document:YunoDict# you will also be able to create your own documentsclassMyDatabase(YunoDatabase):accounts:MyCollectionclassMyClient(YunoClient):production:MyDatabaseclient=MyClient("mongodb+srv://anise:password@yunotest.host.mongodb.net/production")production_database=client.productionaccounts=production_database.accountsaccounts.special_document# this is a YunoDict objectThere is a special__type__attribute which is used to define the default type of the documents in the collection.This is especially useful if all of the documents in the collection share the same schema.You can use type hints to define the schema of special documents (a document which gives global information about the collection for example).It has serveral other methods, which are picked and adapted from the original PyMongoMongoClient.Examplescountis used to count the number of documents matching the given filterfindis used to find documents matching the given filterindexcreates a new index on the collectionaggregatereturns an aggregation of documents following the given pipelinewatchreturns a cursor to watch the database for changes and events.onwill register a callback function which will be called when the specified operation/event is performed on the database.You can also use pythonic syntax to make some operations:Deleting a documentdelcollection.special_document# ordelcollection["special_document"]# will delete the "special_document" documentSet a document (create or replace)collection.special_document={"_id":"special_document","name":"Special document"}# orcollection["special_document"]={"_id":"special_document","name":"Special document"}# will replace the "special_document" document with the one defined aboveVerify if the given document exists"special_document"incollection# will return True if the document (with _id == "special_document") existsUsing objectsAn "object" is what represents any object in a document, or even the document itself.To access objects from another object (the highest hierarchy parent object being the document), all you need to do is access its name as an attribute or an item:document.object_name# ordocument["object_name"]In both case, this will return the same YunoObject object.Thedocument["object_name"]syntax is especially useful if you use an object with its name being of a method overwritten by YunoObject (for examplewatch,on,reload,delete, etc.).You can find a list of overwritten attributes under the__overwritten__attribute.YunoObjectis the class returned byYunoCollectionwhen querying for one.# using the client variable defined beforedocument=accounts.special_document# this is YunoObject objectYou can define your own objects to type hint them (for the same reasons as before)fromyunoimportYunoClient,YunoDatabase,YunoCollection,YunoDictclassCustomObject(YunoDict):hello:str="world"do_not_exist:str="this does not exist"# its default value if not found in dbclassCustomDocument(YunoDict):__lazy__=["hello"]# lazy loaded attributehello:strworld:str="heyhey"a:CustomObject# nested object ^^classSpecialDocument(YunoDict):__lazy__=["specialAttributes"]specialAttributes:list[str]version:strclassCustomCollection(YunoCollection):__type__=CustomDocument# the default type of document in the collectionspecial:SpecialDocument# a special document typeclassCustomDatabase(YunoDatabase):__yuno_test__:CustomCollectionclassCustomClient(YunoClient):test_database:CustomDatabaseclient=CustomClient("mongodb+srv://anise:password@yunotest.host.mongodb.net/production")test_database=client.test_databasetest_collection=test_database.__yuno_test__special_doc=test_collection.specialspecial_doc# this is a SpecialDocument objectdocument=test_collection.any_documentdocument# this is a CustomDocument objectdocument.hello# this is a str object, but lazy loaded (not loaded until needed)document.a# this is CustomObject objectdocument.a.hello# this is a strdocument.a.do_not_exist# this is a str (and if it's not found in the db, it will be the value given by default "this does not exist")There is a special__lazy__attribute which is used to define attributes which won't be loaded until needed.This is especially useful for attributes which are expensive to load or not needed in normal circumstances.You can use type hints to define the schema of some attributes.Objects acts as regular python objects.For example, a YunoDict object can be used as a regular python dict:object# this is a YunoDict objectobject.get("key")# this is a strobject.pop("key")# remove the key from the objectobject.items()# this is a list of tuplesobject.keys()# this is a list of str...forkey,valueinobject.items():print(key,value)delobject["key"]and a YunoList object can be used as a regular python list:object# this is a YunoList objectobject.append("value")# add a value to the listobject.pop()# remove the last value from the listobject.extend(["value1","value2"])# add multiple values to the listobject.index("value")# return the index of the value in the listobject.insert(0,"value")# insert a value at the beginning of the listobject.remove("value")# remove the first value from the listobject.reverse()# reverse the listobject.sort()# sort the list...forvalueinobject:print(value)Some methods don't come from regular Python data types, but are specific to YunoObjects:Examplesdeletedeletes the current object from the databasereloadreloads the current object from the database (replaces the current object with the one from the database)watchreturns a cursor to watch the database for changes and events.onwill register a callback function which will be called when the specified operation/event is performed on the object.Instead of thereloadmethod, you also have a__realtime__attribute which you can set toTrueif you want the object to follow the updates on the database (you will have an object which is always up to date).document# this is a YunoObject (YunoDict, YunoList, etc.) objectdocument.__realtime__=True# this will make the object follow the updates on the databaseclassCustomObject(YunoDict):__realtime__=Truehello:str="world"classCustomCollection(YunoCollection):__type__=CustomObject# any object coming from the CustomCollection collection will be a CustomObject object and will be a "realtime" object, following the updates on the databaseYou can also use pythonic syntax to make some operations:Deleting a documentdelobject.key# ordelobject["key"]# will delete the "key" attribute from the objectSet an attribute (create or replace)object.key={"hello":"world"}# orobject["key"]={"hello":"world"}# will replace the "key" attribute with the one defined aboveVerify if the given document exists"key"inobject# will return True if the attribute existsSecurity packageYuno being a framework, it also provides a set of tools to help you secure your data and logins for your users.They are all located underyuno.security.EncryptEncrypt is a module to encrypt your data using AES.The key can be managed by the database and the token is made recognizable by the given prefix.UsageEncrypting>>>fromyuno.security.encryptimportAES>>>encryption=AES(<yourkey>)>>>encryption.encrypt("hello world")'yuno+312e302e30,1f1d5944d37904e6fe0d129efc11207c,cbb99af1cfb54e08b31d4e,f4876580de57a521791bc0cc527c85cc'You can also use your database to automatically create and store the key.>>>client=YunoClient(...)>>>encryption=AES(key=client)# the key will be randomly generated and stored in the database to be retrieved automatically when needed>>>encryption.encrypt("hello world")'yuno+312e302e30,1f1d5944d37904e6fe0d129efc11207c,cbb99af1cfb54e08b31d4e,f4876580de57a521791bc0cc527c85cc'Decrypting>>>encryption.decrypt("yuno+312e302e30,1f1d5944d37904e6fe0d129efc11207c,cbb99af1cfb54e08b31d4e,f4876580de57a521791bc0cc527c85cc")'hello world'Encryption SchemaHashYou can also hash data and passwords using YunoThis is all located insideyuno.security.hash.DataThe data hasher will use SHA-256 to hash your data.Create aHasherinstance.Four methods are available:hash: Which will automatically detect which method to use.hash_string: To hash a stringhash_bytes: To hash a bytes objecthash_buffer: To hash an IO buffer (file for example).PasswordYuno uses Argon2id, one of the strongest password hashing algorithms, to hash passwords.You can use it by creating an instance ofPasswordHasher.When initializing the PasswordHasher, you will need to pass apepper, a kind of system key, to alter the given passwords and make them unique to the server.Thepepperparameter can also be a database object, in which case a random key will be generated and stored inside the database.UsageHashing>>>fromyuno.security.hashimportPasswordHasher>>>hasher=PasswordHasher(pepper="my_pepper")>>>hasher.hash("my_password")'$argon2id$v=19$m=102400,t=2,p=8$YKj47J7lty47hXrsgIwq0A$XWtTO+CtuSvEWrVph9ZrNQ'The hashed password follows the following schema:PEPPER + PASSWORD + SALT(*)(*) If provided when hashing, it's a per-password bias to make the password unique to an account for example.hasher.hash("my_password",salt="my_salt")the SALT can be for example an account IDVerifyingTwo methods are provided to check if the given password is correct:>>>hasher.verify(password="my_password",hashed="$argon2id$v=19$m=102400,t=2,p=8$YKj47J7lty47hXrsgIwq0A$XWtTO+CtuSvEWrVph9ZrNQ",salt="my_salt")'$argon2id$v=19$m=102400,t=2,p=8$YKj47J7lty47hXrsgIwq0A$XWtTO+CtuSvEWrVph9ZrNQ'This function checks if the given password matches the given hashed password and returns the hashed password if it matches.This returned password can also be slightly different from the original hash because it will sometimes rehash it so make sure to update the hash password with the new one in the database.>>>hasher.is_equal(password="my_password",hashed="$argon2id$v=19$m=102400,t=2,p=8$YKj47J7lty47hXrsgIwq0A$XWtTO+CtuSvEWrVph9ZrNQ",salt="my_salt")TrueDoes basically the same thing as before but will do no verification, rehashing, whatsoever and will returnTrueorFalseinstead of raising an exception if the password don't match.TokensFinally, Yuno can also manage JWT Tokens.A JWT is basically a JSON object encoded which is base64 encoded and signed with a secret key.This way, anyone can read its content but nobody can modify it without the key.This is why Yuno also gives you the option to pass ayuno.security.encrypt.AESinstance to encrypt the tokens with AES and make them unreadable to anyone without the key.UsageWhen initializing theTokenManager, you will need to pass akeywhich is the secret key used to sign the tokens.This can also be a database object, in which case the key will be generated and stored inside the database.You can also provide asignparameter, which will further sign the token from within the its content by adding to each token a random signature.Generating>>>fromyuno.security.tokenimportTokenManager>>>token_manager=TokenManager(key="my_secret_key")>>>token_manager.generate(user="id-123",username="username-123",roles=["admin","user"])# and any extra data'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDYwNDEwNDcsImV4cCI6MTY0NjEyNzQ0NywidXNlciI6ImlkLTEyMyIsInVzZXJuYW1lIjoidXNlcm5hbWUtMTIzIiwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl19.2ddAuZU7ozW9_F2dz9JKZbxdsNZlFbkjm5WYiuJVuUo'with encryption>>>fromyuno.security.encryptimportAES>>>aes=AES()>>>token_manager.generate(user="id-123",encryption=aes)'yuno+312e302e30,667dfc91d461177e673582969809d4a8,fdf3e9a34532ca63389a15bcae8d7c0373221d12e617b56a9136b1d9288f1f2da3020390c5657451357ab73607fc2040a87f4194b1abd84e8b6e9767e2ffc04ae5f70e2869c60138f8a0922f2e2f9b31f6e0913bb6b2b3e7e7b6679e55c865a4a9598016fc713e6eb48353cc61bc9954c897794ff4f29f50e0d2282a930e26b7ee7bb547428bbedef74e88a4f827eeab5564c883b7,82beee1f51fc1c1de408cc4a49c15f13'Decoding>>>token_manager.decode("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDYwNDEwNDcsImV4cCI6MTY0NjEyNzQ0NywidXNlciI6ImlkLTEyMyIsInVzZXJuYW1lIjoidXNlcm5hbWUtMTIzIiwicm9sZXMiOlsiYWRtaW4iLCJ1c2VyIl19.2ddAuZU7ozW9_F2dz9JKZbxdsNZlFbkjm5WYiuJVuUo"){'iat':1646037825,'exp':1646124194,'user':'id-123','username':'username-123','roles':['admin','user']}with encryption>>>token_manager.decode("yuno+312e302e30,667dfc91d461177e673582969809d4a8,fdf3e9a34532ca63389a15bcae8d7c0373221d12e617b56a9136b1d9288f1f2da3020390c5657451357ab73607fc2040a87f4194b1abd84e8b6e9767e2ffc04ae5f70e2869c60138f8a0922f2e2f9b31f6e0913bb6b2b3e7e7b6679e55c865a4a9598016fc713e6eb48353cc61bc9954c897794ff4f29f50e0d2282a930e26b7ee7bb547428bbedef74e88a4f827eeab5564c883b7,82beee1f51fc1c1de408cc4a49c15f13"){'iat':1646041175,'exp':1646127575,'user':'id-123'}When using thesignparameter, an extra signature will be added to the token.>>>token_manager=TokenManager("my_key",sign="my_sign")>>>token_manager.generate(sub="id-123",username="username-123",roles=["admin","user"])'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDYwNDMwOTgsImV4cCI6MTY0NjEyOTQ5OCwidXNlciI6ImlkLTEyMyIsInJhbmQiOiI1N2IyZmVhODNlNjE1M2ZjIiwic2lnbiI6Ijg5MjQyZTdlOWNhMzAwMGU4M2NiZWUxMTQxZWQ4MTFhNGQ3M2NkNTliYTdkODE5ZjVkOTg2YzEzYzg5OGNjMzIiLCJ1c2VybmFtZSI6InVzZXJuYW1lLTEyMyIsInJvbGVzIjpbImFkbWluIiwidXNlciJdfQ.t0bL6_cqpZOeY2oJoBURSMhvagjRT3b_KlCBTFjFduI'>>>token_manager.decode('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDYwNDMwOTgsImV4cCI6MTY0NjEyOTQ5OCwidXNlciI6ImlkLTEyMyIsInJhbmQiOiI1N2IyZmVhODNlNjE1M2ZjIiwic2lnbiI6Ijg5MjQyZTdlOWNhMzAwMGU4M2NiZWUxMTQxZWQ4MTFhNGQ3M2NkNTliYTdkODE5ZjVkOTg2YzEzYzg5OGNjMzIiLCJ1c2VybmFtZSI6InVzZXJuYW1lLTEyMyIsInJvbGVzIjpbImFkbWluIiwidXNlciJdfQ.t0bL6_cqpZOeY2oJoBURSMhvagjRT3b_KlCBTFjFduI'){'iat':1646043098,'exp':1646129498,'sub':'id-123','rand':'57b2fea83e6153fc','sign':'89242e7e9ca3000e83cbee1141ed811a4d73cd59ba7d819f5d986c13c898cc32','username':'username-123','roles':['admin','user']}Notice that arandandsignfields are added to the token.Therandfield is random data as hex generated to be used in thesignfield.Thesignfield is the signature of the token.It is a SHA-256 hashed concatenation of therandfield and thesignprovided to the token manager.ConsiderationsThe goal here is to increase the number of keys and biases to make cracking the tokens and hashes way harder to any attacker.As you can see, the JWT has the most layers because it is exposed to the public.You should protect your databases and APIs and try to use some key rotation mechanism for maximum security.How it worksYuno works on top of PyMongo to make all of the operations to MongoDB.It also uses PyJWT for the JWT encoding and decoding, Argon2 for the hashing and encryption of the passwords, and PyCryptodome for the AES encryption.DeploymentThis module is currently in development and might contain bugs.Feel free to use it in production if you feel like it is suitable for your production even if you may encounter issues.ContributingPull requests are welcome. For major changes, please open a discussion first to discuss what you would like to change.Please make sure to update the tests as appropriate.Built WithPyMongo- To connect to MongoDB databases and make operationspsutil- For cross-platform process managementPyYAML- To parse YAML files (MongoDB configuration files)argon2-cffi- To hash and encrypt passwords with Argon2idPyJWT- To encode and decode JWT tokensPyCryptodome- To AES encrypt and decrypt dataAuthorsAnime no Sekai-Initial work-AnimenosekaiLicenseThis project is licensed under the MIT License - see theLICENSEfile for details.