# Python quickstart

Location: [gRPC API](https://developer.m-files.com/APIs/gRPC-API/index.md) > [Guides](https://developer.m-files.com/APIs/gRPC-API/index.md#guides)

HTML version: https://developer.m-files.com/APIs/gRPC-API/guides/quickstart-python/

In about 15 minutes you generate the Python stubs of the M-Files gRPC API, configure a
connection, and run a program that logs in to a vault and prints its 10 most recently modified
documents.

## Prerequisites

* Python 3.10 or newer.
* A vault you can reach and its GUID, with braces (`{7BBEA0BC-40D8-4BCB-A67D-8A3ACFBDB575}`).
  Don't know the GUID? [Find the vault by its name](https://developer.m-files.com/APIs/gRPC-API/guides/connect/index.md#find-the-vault-by-its-name)
  lists the vaults you can log in to, or ask your M-Files administrator.
  The endpoint:
  * **M-Files Cloud:** the vault's host name; the gRPC endpoint is that host on port 443, with TLS.
  * **On-premises:** the M-Files Server host and its gRPC port, 7766 unless your administrator
    changed it. See [Cloud and on-premises](https://developer.m-files.com/APIs/gRPC-API/runtime/deployments/index.md).
* A way to log in: an M-Files user account with a password, or single sign-on through M-Files
  Login Service. See [Authentication and sessions](https://developer.m-files.com/APIs/gRPC-API/runtime/authentication/index.md).

## 1. Get the proto files

Download the **[gRPC API bundle](https://developer.m-files.com/APIs/gRPC-API/downloads/mfiles-grpc-api-bundle.zip)** and unpack it. Its
`proto` folder is the published API definition: one file per service (`mfiles/IRPCSearch.proto`, ...) plus the shared `mfiles/structs.proto` and
`mfiles/enums.proto`, all in the package `MFiles`, with documentation comments.

An M-Files installation also contains a proto file, in `Common\Web\GRPC\proto` under the
installation folder. That is the legacy single combined file. Prefer the bundle: these samples
and all code in the documentation use the bundle's file and module names (`mfiles.IRPCSearch_pb2`,
`mfiles.structs_pb2`), which the combined file does not produce.

## 2. Generate the stubs

The samples are in the `samples/python` folder of the bundle, next to the `proto` folder.

```shell
cd samples/python
python -m venv .venv
.venv/bin/pip install -r requirements.txt     # Windows: .venv\Scripts\pip install ...
.venv/bin/python generate.py                  # Windows: .venv\Scripts\python generate.py
```

`generate.py` runs `grpc_tools.protoc` on every file of the bundle and writes the stubs into
`gen/`. It finds the bundle at `../../proto`; pass another folder as an argument or set
`MFILES_PROTO_DIR`. The pinned versions are in `requirements.txt` (`grpcio`, `grpcio-tools`,
`protobuf`).

To generate the stubs without `generate.py`, include the files in `mfiles/api/` too: the service
files import `mfiles/api/annotations.proto`, and a `mfiles/*.proto` pattern misses it (the import
then fails with `No module named 'mfiles.api'`):

```shell
python -m grpc_tools.protoc -I proto --python_out=gen --grpc_python_out=gen \
    proto/mfiles/*.proto proto/mfiles/api/*.proto
```

## 3. Configure the connection

The samples read environment variables. For M-Files Cloud (PowerShell):

```powershell
$env:MFILES_ADDRESS = "myvault.example.com:443"
$env:MFILES_VAULT = "{00000000-0000-0000-0000-000000000000}"
$env:MFILES_USERNAME = "me"
$env:MFILES_PASSWORD = Read-Host -Prompt 'Password'
```

For an on-premises server without TLS (bash):

```shell
export MFILES_ADDRESS=myserver:7766 MFILES_TLS=false
export MFILES_VAULT='{00000000-0000-0000-0000-000000000000}'
export MFILES_USERNAME=me
read -rs MFILES_PASSWORD && export MFILES_PASSWORD
```

| Variable | Meaning |
|---|---|
| `MFILES_ADDRESS` | `host:port` of the gRPC endpoint |
| `MFILES_TLS` | `true` (default) or `false` for a server without TLS |
| `MFILES_CA_CERT` | Optional: PEM file of root certificates to trust, for a self-signed server certificate |
| `MFILES_VAULT` | The vault GUID, with braces |
| `MFILES_USERNAME`, `MFILES_PASSWORD` | An M-Files user account (M-Files authentication) |
| `MFILES_TOKEN`, `MFILES_AUTH_PLUGIN`, `MFILES_AUTH_SCOPE` | Instead of a password: an SSO token, the authentication plugin's name and its configuration scope |

Use `MFILES_TLS=false` only for a server that has no certificate, such as a lab installation.

**Single sign-on.** A vault that signs users in through M-Files Login Service, as is typical on
M-Files Cloud, refuses a password login with "Authentication failed.". Set `MFILES_TOKEN` to an
access token from the Login Service, and `MFILES_AUTH_PLUGIN` and `MFILES_AUTH_SCOPE` to the plugin
and scope that the vault returns.
[M-Files Login Service](https://developer.m-files.com/APIs/gRPC-API/runtime/authentication/index.md#m-files-login-service) shows how to get the
token; the Python sample `recipes/login_service.py` does it end to end.

## 4. Run it

```shell
.venv/bin/python quickstart.py
```

The output lists the documents, newest first. In the M-Files Sample Vault it starts like this:

```text
Project Plan (template) (ID 445, version 1, modified 2012-06-07 12:01:57 UTC)
Project Tasks - CRM Development (ID 444, version 1, modified 2012-06-07 11:54:29 UTC)
Corporate Presentation (ID 443, version 1, modified 2012-06-07 11:40:14 UTC)
...
```

## The code

```python
from mfiles_client import MFilesClient, run  # Also makes the stubs in gen/ importable.
from mfiles_values import is_not_deleted, is_object_type

from mfiles import IRPCSearch_pb2, IRPCSearch_pb2_grpc
from mfiles import enums_pb2 as enums
from mfiles import structs_pb2 as structs


def main() -> None:
    # Connect and log in with the MFILES_* settings. Leaving the block logs out.
    with MFilesClient.from_env("M-Files gRPC quickstart") as client:
        request = IRPCSearch_pb2.SearchObjectsRequest(
            # Documents (object type 0) that are not deleted.
            conditions=[structs.SearchConditionArray(
                value=[is_object_type(0), is_not_deleted()])],
            # Newest first by property 21, "Last modified". The server sorts, then takes 10.
            sorting=structs.SearchResultsSorting(rules=structs.SortRuleValueArray(values=[
                structs.SortRule(key=21, options=enums.SortRuleFlags(sort_descending=True))])),
            limit=10)

        # Service stubs run on the client's channel, which adds the session to each call.
        search = client.stub(IRPCSearch_pb2_grpc.IRPCSearchStub)
        response = search.SearchObjects(request)

        for item in response.results:
            version = item.object.version_info
            print(f"{version.title} (ID {item.object.object_info.obj_id.item_id.internal_id}, "
                  f"version {version.version.internal_version}, "
                  f"modified {version.last_modified_at_utc.ToDatetime():%Y-%m-%d %H:%M:%S} UTC)")


if __name__ == "__main__":
    run(main)
```

*Source: [python/quickstart.py](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/python/quickstart.py)*

* `MFilesClient.from_env` opens the channel and logs in. Leaving the `with` block logs out and
  closes the channel.
* Every service has a generated stub, created on the client's channel with `client.stub(...)`.
* [SearchObjects](https://developer.m-files.com/APIs/gRPC-API/Reference/MFiles.IRPCSearch/SearchObjects/index.md) takes exactly one
  [SearchConditionArray](https://developer.m-files.com/APIs/gRPC-API/Reference/Messages/MFiles.SearchConditionArray/index.md) of
  [SearchCondition](https://developer.m-files.com/APIs/gRPC-API/Reference/Messages/MFiles.SearchCondition/index.md) items, which must all match. Without the
  "not deleted" condition, users who may see deleted objects, such as administrators, get them
  too.
* `limit=10` with a sort order returns the first 10 of all matches in that order.
* The results are [SearchResultsItem](https://developer.m-files.com/APIs/gRPC-API/Reference/Messages/MFiles.SearchResultsItem/index.md) messages; `object`
  holds the version information (`version_info`) and the object information (`object_info`).
* A failed call raises `MFilesError`, which names the M-Files error code.

`is_object_type` and `is_not_deleted` are small builders from `mfiles_values.py`. They show the
message shapes of the two conditions; lookups to object types use the value list ID -11:

```python
def property_condition(property_def: int, condition_type: int,
                       value: structs.TypedValue) -> structs.SearchCondition:
    """A condition on a property value, e.g. property 100 (Class) equals a lookup."""
    return structs.SearchCondition(
        expression=structs.Expression(
            type=enums.EXPRESSION_TYPE_PROPERTY_VALUE,
            data=structs.ExpressionUnion(property_value=structs.ExpressionUnion.PropertyValueData(
                property_def=property_def))),
        type=condition_type,
        value=value)


def status_condition(status: int, condition_type: int,
                     value: structs.TypedValue) -> structs.SearchCondition:
    """A condition on a status value, e.g. the object type or the deleted flag."""
    return structs.SearchCondition(
        expression=structs.Expression(
            type=enums.EXPRESSION_TYPE_STATUS_VALUE,
            data=structs.ExpressionUnion(status_value=structs.ExpressionUnion.StatusValueData(
                type=status))),
        type=condition_type,
        value=value)


def is_object_type(object_type: int) -> structs.SearchCondition:
    """Objects of one object type. Value list -11 lists the object types."""
    return status_condition(enums.STATUS_TYPE_OBJECT_TYPE, enums.CONDITION_TYPE_EQUAL,
                            lookup(-11, object_type))


def is_not_deleted() -> structs.SearchCondition:
    """Objects that are not deleted."""
    return status_condition(enums.STATUS_TYPE_IS_DELETED, enums.CONDITION_TYPE_EQUAL,
                            boolean(False))
```

*Source: [python/mfiles_values.py](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/python/mfiles_values.py)*

More about conditions: [Search](https://developer.m-files.com/APIs/gRPC-API/concepts/search/index.md).

## What the helper does for you

`mfiles_client.py` hides the parts that the proto files cannot express.

It opens the channel, with TLS unless `MFILES_TLS` is false, and sets the message size limits that
M-Files' own client uses: responses of any size, requests up to 48 MiB (the server's default
limit):

```python
options = [("grpc.max_receive_message_length", MAX_RECEIVE_MESSAGE_BYTES),
           ("grpc.max_send_message_length", MAX_SEND_MESSAGE_BYTES)]
if config.tls:
    root_certificates = Path(config.ca_cert).read_bytes() if config.ca_cert else None
    credentials = grpc.ssl_channel_credentials(root_certificates=root_certificates)
    self._channel = grpc.secure_channel(config.address, credentials, options=options)
else:
    self._channel = grpc.insecure_channel(config.address, options=options)
self.channel = grpc.intercept_channel(self._channel, _MetadataInterceptor(self))
```

*Source: [python/mfiles_client.py](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/python/mfiles_client.py)*

It logs in with [LogIn](https://developer.m-files.com/APIs/gRPC-API/Reference/MFiles.IRPCLogin/LogIn/index.md). The response's `session_data.session_id`
(48 bytes) identifies the session:

```python
request = IRPCLogin_pb2.LogInRequest(
    environment_data=structs_pb2.EnvironmentData(
        client_name=self.client_name, host_platform="Python"),
    client_data=self._client_data(),
    login_data=structs_pb2.LoginData(
        server_hostname=self.config.host, vault_guid=self.config.vault),
    authentication_data=authentication)
response = self.login.LogIn(request)
if not response.session_data.session_id:
    raise RuntimeError("LogIn did not complete: the authentication needs more steps")
# From now on, the interceptor sends this in "mfiles-session-id-bin" on every call.
self.session_id = response.session_data.session_id
```

*Source: [python/mfiles_client.py](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/python/mfiles_client.py)*

With a user name and password, `authentication_data` is:

```python
authentication = structs_pb2.AuthDataClient(
    type=enums_pb2.AUTH_DATA_TYPE_CREDENTIALS,
    data=structs_pb2.AuthDataClientUnion(credentials=structs_pb2.Credentials(
        type=enums_pb2.CREDENTIALS_TYPE_MFILES, username=username, password=password)))
```

*Source: [python/mfiles_client.py](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/python/mfiles_client.py)*

Its interceptor adds call metadata to every call: `mfiles-is-remote-call: true`, a new random
16-byte `mfiles-activity-id-bin`, and after login the session ID as `mfiles-session-id-bin`.
None of the request messages has a field for the session. A call that needs a session and does
not have one fails with `UNAUTHENTICATED`. M-Files Cloud sets the cookie `mfilesmsm` to keep a
client on one server instance; gRPC does not keep cookies, so the interceptor stores `set-cookie`
values from the response metadata and sends them back in a `cookie` header on every later call.
See [Call metadata](https://developer.m-files.com/APIs/gRPC-API/runtime/call-metadata/index.md#cookies-and-stickiness).

When a call fails, the interceptor raises `MFilesError`, decoded from the
`mfiles-error-serialized-bin` and `mfiles-error-text-bin` trailers: the gRPC status (`status`),
the message (`message`), the M-Files error code of the top-level error (`hresult`; the last
entry of the error stack that is not the generic `0x80040001`), every code of the stack
(`codes`) and `has(code)`:

```python
self.long_message: str = trailers.get(ERROR_TEXT_KEY, b"").decode("utf-8", "replace")
self.stack = list(structs_pb2.RPCErrorInfoArray.FromString(
    trailers.get(ERROR_STACK_KEY, b"")).value)
self.codes = [entry.error_code & 0xFFFFFFFF for entry in self.stack]  # cause first
# The top-level error, skipping entries that only pass the error on.
self.hresult: Optional[int] = next(
    (code for code in reversed(self.codes) if code != E_PASS_THROUGH),
    self.codes[-1] if self.codes else None)
```

*Source: [python/mfiles_client.py](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/python/mfiles_client.py)*

Catch it like this:

```python
try:
    objects.GetObjectInfoAndProperties(request)
except MFilesError as error:
    if not error.has(0x8004000B):  # not found
        raise
    print(f"{error.hresult_hex}: {error.message}")
```

[Errors](https://developer.m-files.com/APIs/gRPC-API/runtime/errors/index.md) lists the common codes and what to do about each.

At the end, `close()` calls [LogOut](https://developer.m-files.com/APIs/gRPC-API/Reference/MFiles.IRPCLogin/LogOut/index.md) and closes the channel.

## If it does not work

| Symptom | Likely cause |
|---|---|
| `UNAVAILABLE`, "Tls handshake failed ... WRONG_VERSION_NUMBER" | TLS against a server without TLS: set `MFILES_TLS=false`, only for a lab server on a trusted network |
| `UNIMPLEMENTED`, "Received http2 header with status: 404" | A plaintext call to a TLS endpoint, such as M-Files Cloud: remove `MFILES_TLS=false` |
| `UNAVAILABLE`, "Connection refused" | Wrong host or port, or a firewall |
| `UNKNOWN`, M-Files code `0x8004001A`, "Authentication failed." | Wrong user name or password, or the vault uses single sign-on (see step 3) |
| `UNKNOWN`, M-Files code `0x80040020`, "The specified document vault does not exist." | Wrong `MFILES_VAULT` |
| `UNAUTHENTICATED` | No session or an expired one: log in again |

## Next steps

* [Connect](https://developer.m-files.com/APIs/gRPC-API/guides/connect/index.md): versions, capabilities, sessions and keep-alive.
* [Vault structure](https://developer.m-files.com/APIs/gRPC-API/guides/vault-structure/index.md): find the IDs of object types, classes and properties.
* [Search](https://developer.m-files.com/APIs/gRPC-API/guides/search/index.md), [Read an object](https://developer.m-files.com/APIs/gRPC-API/guides/read-object/index.md), [Download a file](https://developer.m-files.com/APIs/gRPC-API/guides/download-file/index.md).
* [Create a document](https://developer.m-files.com/APIs/gRPC-API/guides/create-document/index.md), [Update properties](https://developer.m-files.com/APIs/gRPC-API/guides/update-properties/index.md),
  [Change the workflow state](https://developer.m-files.com/APIs/gRPC-API/guides/workflow-state/index.md).
* [Page through results](https://developer.m-files.com/APIs/gRPC-API/guides/paging/index.md) and [Handle errors](https://developer.m-files.com/APIs/gRPC-API/guides/errors/index.md).
