mfiles-grpc 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. mfiles_grpc-1.0.0/.github/workflows/ci.yml +52 -0
  2. mfiles_grpc-1.0.0/.github/workflows/publish.yml +91 -0
  3. mfiles_grpc-1.0.0/.github/workflows/tag.yml +71 -0
  4. mfiles_grpc-1.0.0/.gitignore +19 -0
  5. mfiles_grpc-1.0.0/LICENSE +21 -0
  6. mfiles_grpc-1.0.0/PKG-INFO +345 -0
  7. mfiles_grpc-1.0.0/README.md +310 -0
  8. mfiles_grpc-1.0.0/pyproject.toml +65 -0
  9. mfiles_grpc-1.0.0/scripts/generate_stubs.py +70 -0
  10. mfiles_grpc-1.0.0/scripts/live_object_test.py +168 -0
  11. mfiles_grpc-1.0.0/setup.cfg +4 -0
  12. mfiles_grpc-1.0.0/src/mfiles_grpc/__init__.py +8 -0
  13. mfiles_grpc-1.0.0/src/mfiles_grpc/__main__.py +115 -0
  14. mfiles_grpc-1.0.0/src/mfiles_grpc/_generated/__init__.py +1 -0
  15. mfiles_grpc-1.0.0/src/mfiles_grpc/_generated/mfilesCombinedWithDataPush_pb2.py +4878 -0
  16. mfiles_grpc-1.0.0/src/mfiles_grpc/_generated/mfilesCombinedWithDataPush_pb2_grpc.py +32611 -0
  17. mfiles_grpc-1.0.0/src/mfiles_grpc/client.py +266 -0
  18. mfiles_grpc-1.0.0/src/mfiles_grpc/config.py +119 -0
  19. mfiles_grpc-1.0.0/src/mfiles_grpc/objects.py +152 -0
  20. mfiles_grpc-1.0.0/src/mfiles_grpc/proto.py +19 -0
  21. mfiles_grpc-1.0.0/src/mfiles_grpc/sso.py +303 -0
  22. mfiles_grpc-1.0.0/src/mfiles_grpc/structure.py +57 -0
  23. mfiles_grpc-1.0.0/src/mfiles_grpc/values.py +131 -0
  24. mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/PKG-INFO +345 -0
  25. mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/SOURCES.txt +34 -0
  26. mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/dependency_links.txt +1 -0
  27. mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/entry_points.txt +2 -0
  28. mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/requires.txt +10 -0
  29. mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/scm_file_list.json +30 -0
  30. mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/scm_version.json +8 -0
  31. mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/top_level.txt +1 -0
  32. mfiles_grpc-1.0.0/tests/test_client.py +233 -0
  33. mfiles_grpc-1.0.0/tests/test_config.py +129 -0
  34. mfiles_grpc-1.0.0/tests/test_objects.py +93 -0
  35. mfiles_grpc-1.0.0/tests/test_sso.py +226 -0
  36. mfiles_grpc-1.0.0/tests/test_values.py +67 -0
@@ -0,0 +1,52 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
18
+ steps:
19
+ - uses: actions/checkout@v5
20
+ - uses: actions/setup-python@v6
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - run: python -m pip install --upgrade pip
24
+ - run: pip install -e ".[dev]"
25
+ - run: flake8 --max-line-length 120 --extend-exclude src/mfiles_grpc/_generated src tests scripts
26
+ - run: pytest -q
27
+
28
+ build:
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: actions/checkout@v5
32
+ with:
33
+ fetch-depth: 0 # setuptools-scm needs the tags
34
+ - uses: actions/setup-python@v6
35
+ with:
36
+ python-version: "3.13"
37
+ - run: python -m pip install --upgrade build twine
38
+ - run: python -m build
39
+ - run: twine check --strict dist/*
40
+ # The package must never carry the .proto itself, only the stubs generated from it.
41
+ - name: No .proto in the package
42
+ run: |
43
+ for f in dist/*; do
44
+ if python -m zipfile -l "$f" 2>/dev/null | grep -q '\.proto$' || \
45
+ tar -tzf "$f" 2>/dev/null | grep -q '\.proto$'; then
46
+ echo "::error::$f contains a .proto file"; exit 1
47
+ fi
48
+ done
49
+ - uses: actions/upload-artifact@v4
50
+ with:
51
+ name: dist
52
+ path: dist/
@@ -0,0 +1,91 @@
1
+ name: Publish
2
+
3
+ # Trusted Publishing: PyPI and TestPyPI trust this workflow, so no API token is stored.
4
+ # A GitHub release published on a v* tag → PyPI, and the files are attached to the release.
5
+ # Run by hand on a branch (Actions → Publish) → a .devN version on TestPyPI.
6
+ # The Tag workflow creates the v* tags on merges to main. The version comes from the tag
7
+ # (setuptools-scm).
8
+
9
+ on:
10
+ release:
11
+ types: [published]
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v5
22
+ with:
23
+ fetch-depth: 0 # setuptools-scm needs the tags
24
+ - uses: actions/setup-python@v6
25
+ with:
26
+ python-version: "3.13"
27
+ - run: python -m pip install --upgrade build twine
28
+ - run: python -m build
29
+ - name: Built version matches the tag
30
+ if: startsWith(github.ref, 'refs/tags/v')
31
+ run: |
32
+ if [ ! -f "dist/mfiles_grpc-${GITHUB_REF_NAME#v}.tar.gz" ]; then
33
+ echo "::error::Tag $GITHUB_REF_NAME, but built $(ls dist)"; exit 1
34
+ fi
35
+ - run: twine check --strict dist/*
36
+ # The package must never carry the .proto itself, only the stubs generated from it.
37
+ - name: No .proto in the package
38
+ run: |
39
+ for f in dist/*; do
40
+ if python -m zipfile -l "$f" 2>/dev/null | grep -q '\.proto$' || \
41
+ tar -tzf "$f" 2>/dev/null | grep -q '\.proto$'; then
42
+ echo "::error::$f contains a .proto file"; exit 1
43
+ fi
44
+ done
45
+ - uses: actions/upload-artifact@v4
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+
50
+ testpypi:
51
+ if: github.event_name == 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/')
52
+ needs: build
53
+ runs-on: ubuntu-latest
54
+ permissions:
55
+ id-token: write
56
+ steps:
57
+ - uses: actions/download-artifact@v5
58
+ with:
59
+ name: dist
60
+ path: dist/
61
+ - uses: pypa/gh-action-pypi-publish@release/v1
62
+ with:
63
+ repository-url: https://test.pypi.org/legacy/
64
+
65
+ pypi:
66
+ if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v')
67
+ needs: build
68
+ runs-on: ubuntu-latest
69
+ permissions:
70
+ id-token: write
71
+ steps:
72
+ - uses: actions/download-artifact@v5
73
+ with:
74
+ name: dist
75
+ path: dist/
76
+ - uses: pypa/gh-action-pypi-publish@release/v1
77
+
78
+ attach:
79
+ needs: pypi
80
+ runs-on: ubuntu-latest
81
+ permissions:
82
+ contents: write
83
+ steps:
84
+ - uses: actions/download-artifact@v5
85
+ with:
86
+ name: dist
87
+ path: dist/
88
+ - run: gh release upload "$GITHUB_REF_NAME" dist/*
89
+ env:
90
+ GH_TOKEN: ${{ github.token }}
91
+ GH_REPO: ${{ github.repository }}
@@ -0,0 +1,71 @@
1
+ name: Tag
2
+
3
+ # When a pull request is merged to main, tag the merge commit with the next version. The pull
4
+ # request's labels choose the step:
5
+ # none → patch 1.2.3 → 1.2.4
6
+ # minor-version → minor 1.2.3 → 1.3.0
7
+ # major-version → major 1.2.3 → 2.0.0
8
+ # The first tag is v1.0.0.
9
+ #
10
+ # pull_request_target runs this file as it is on main, with a token that can write. It never
11
+ # checks out or runs the pull request's code; it reads only the labels and the merge commit.
12
+ # The "release tags" ruleset stops v* tags from being moved or deleted, but not created:
13
+ # GITHUB_TOKEN cannot be given a ruleset bypass.
14
+ #
15
+ # Tagging publishes nothing: creating a GitHub release on the tag starts Publish.
16
+
17
+ on:
18
+ pull_request_target:
19
+ types: [closed]
20
+ branches: [main]
21
+
22
+ permissions: {}
23
+
24
+ concurrency:
25
+ group: tag
26
+ cancel-in-progress: false
27
+
28
+ jobs:
29
+ tag:
30
+ if: github.event.pull_request.merged
31
+ runs-on: ubuntu-latest
32
+ permissions:
33
+ contents: write
34
+ env:
35
+ GH_TOKEN: ${{ github.token }}
36
+ GH_REPO: ${{ github.repository }}
37
+ SHA: ${{ github.event.pull_request.merge_commit_sha }}
38
+ LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
39
+ steps:
40
+ - name: Next version
41
+ id: next
42
+ run: |
43
+ gh api --paginate "repos/$GH_REPO/git/matching-refs/tags/v" -q '.[] | .ref + " " + .object.sha' |
44
+ python3 -c '
45
+ import json, os, re, sys
46
+ labels = set(json.loads(os.environ["LABELS"]))
47
+ versions = []
48
+ for line in sys.stdin:
49
+ ref, sha = line.split()
50
+ if m := re.fullmatch(r"refs/tags/v(\d+)\.(\d+)\.(\d+)", ref):
51
+ if sha == os.environ["SHA"]:
52
+ sys.exit(f"::error::{ref} already tags {sha}")
53
+ versions.append(tuple(map(int, m.groups())))
54
+ if not versions:
55
+ major, minor, patch = 1, 0, 0
56
+ else:
57
+ major, minor, patch = max(versions)
58
+ if "major-version" in labels:
59
+ major, minor, patch = major + 1, 0, 0
60
+ elif "minor-version" in labels:
61
+ minor, patch = minor + 1, 0
62
+ else:
63
+ patch += 1
64
+ print(f"tag=v{major}.{minor}.{patch}")
65
+ ' >> "$GITHUB_OUTPUT"
66
+ - name: Create the tag
67
+ run: |
68
+ gh api "repos/$GH_REPO/git/refs" -f ref="refs/tags/$TAG" -f sha="$SHA" --silent
69
+ echo "Tagged $SHA as $TAG" >> "$GITHUB_STEP_SUMMARY"
70
+ env:
71
+ TAG: ${{ steps.next.outputs.tag }}
@@ -0,0 +1,19 @@
1
+ # Working notes, not published
2
+ PLAN.md
3
+
4
+ # Build and test leftovers
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ __pycache__/
9
+ .pytest_cache/
10
+ .venv/
11
+ venv/
12
+
13
+ # Never commit a real configuration: it holds the vault password
14
+ client-config.toml
15
+ proxide.log
16
+
17
+ # M-Files' protocol file is not published; only the stubs generated from it are.
18
+ # Keep a local copy here for scripts/generate_stubs.py.
19
+ *.proto
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jari Turkia
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,345 @@
1
+ Metadata-Version: 2.4
2
+ Name: mfiles-grpc
3
+ Version: 1.0.0
4
+ Summary: Python client for the M-Files gRPC API
5
+ Author-email: Jari Turkia <jatu@hqcodeshop.fi>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/M-Files/mfiles-grpc-python
8
+ Project-URL: Source, https://github.com/M-Files/mfiles-grpc-python
9
+ Project-URL: Issues, https://github.com/M-Files/mfiles-grpc-python/issues
10
+ Keywords: M-Files,gRPC,document management,ECM
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Office/Business
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: grpcio>=1.84.0
26
+ Requires-Dist: protobuf<8,>=7.35.1
27
+ Provides-Extra: dev
28
+ Requires-Dist: grpcio-tools>=1.84.0; extra == "dev"
29
+ Requires-Dist: pytest>=9.0.2; extra == "dev"
30
+ Requires-Dist: black>=26.3.1; extra == "dev"
31
+ Requires-Dist: flake8>=7.3.0; extra == "dev"
32
+ Requires-Dist: build; extra == "dev"
33
+ Requires-Dist: twine; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # mfiles-grpc
37
+
38
+ Python client for the M-Files gRPC API: the protocol M-Files' own clients use.
39
+ It reaches parts of the vault the REST API (MFWS) does not, most usefully the
40
+ metadata structure. `POST /REST/structure/properties` answers HTTP 405, while
41
+ gRPC has `IRPCPropertyDefsAdmin.AddPropertyDef`, `AddObjectClass`,
42
+ `IRPCObjectTypesAdmin.AddObjectType` and a declarative whole-structure
43
+ get/set (`IRPCDeclarativeMetadataStructure`).
44
+
45
+ > **Not a supported public API.** The protocol comes from the M-Files Desktop
46
+ > client install and can change with any server update. Regenerate the stubs
47
+ > (see [Source of the .proto](https://github.com/M-Files/mfiles-grpc-python#source-of-the-proto)) after upgrading, and run the tests.
48
+
49
+ ## Status
50
+
51
+ | What | State |
52
+ |---|---|
53
+ | gRPC on the REST host and port (443 on M-Files Cloud), path `/MFiles.<Service>/<Method>` | verified live |
54
+ | The `.proto` matches the server (live replies decode field for field) | verified live |
55
+ | Anonymous calls (`GetServerCapabilities`, `GetPublicKeyAnonymous`) | verified live |
56
+ | `LogIn` with user name and password → 48-byte session ID | verified live |
57
+ | Using that session on later calls | verified live (2026-09-24) |
58
+ | Connecting through a capturing proxy (`address`, `ca-cert`; Proxide) | verified live (2026-09-25) |
59
+ | SSO: reading the vault's OAuth settings (`auth-config`), anonymously | verified live (2026-09-24) |
60
+ | SSO: browser sign-in, `LogIn` with the token, session accepted | verified live (2026-09-24) |
61
+ | Structure helpers, reading object properties | verified live |
62
+ | Writing object properties (`set_properties`, with `expected_version` guard) | verified live (2026-09-24) |
63
+ | `create_object`, `remove_properties`, `delete_object`, `destroy_object` | verified live (2026-09-24) |
64
+
65
+ Verified against an M-Files Cloud vault. `scripts/live_object_test.py` repeats
66
+ the object checks against your vault: it creates a throwaway object, changes it,
67
+ adds values, checks that a write aimed at an old version is refused, empties one
68
+ value and removes another, then destroys the object. It **writes to the vault**;
69
+ on failure it prints the ID it left behind. `--keep` skips the delete. You name
70
+ an object type and properties of your own; see `--help`. For example:
71
+
72
+ ```
73
+ python scripts/live_object_test.py --config client-config.toml --object-type eBook \
74
+ --integer "Page count" --optional-integer "Publishing year" --multiline Source
75
+ ```
76
+
77
+ What it established:
78
+
79
+ * Creating an object needs `value_metadata` on every value (the helper adds
80
+ it); without it the server answers "Type mismatch."
81
+ * An object of a type that can have files cannot be created without
82
+ `Single file` (22).
83
+ * A property the class lists cannot be removed, only set empty (null).
84
+ `remove_properties` works only for properties the class does not list, such
85
+ as `Keywords` (26).
86
+
87
+ ## How the session travels
88
+
89
+ `LogIn` returns a session ID, but none of the request messages has a field
90
+ for it: it travels in call metadata (HTTP/2 headers). The `.proto` does not
91
+ say so; the header names came from M-Files. The client adds these to every call:
92
+
93
+ | Header | Value |
94
+ |---|---|
95
+ | `mfiles-session-id-bin` | `session_data.session_id` from `LogIn`, unchanged (after logging in) |
96
+ | `mfiles-is-remote-call` | `true` |
97
+ | `mfiles-activity-id-bin` | a new GUID for each call, for tracing |
98
+
99
+ gRPC base64-encodes `-bin` values itself. A call that needs a session and does
100
+ not carry `mfiles-session-id-bin` fails with UNAUTHENTICATED; a REST
101
+ `X-Authentication` token is not accepted in its place.
102
+
103
+ `mfiles-grpc check-session` logs in and makes one read-only call to confirm it.
104
+
105
+ ## Logging in with SSO
106
+
107
+ Human users sign in through the vault's identity provider, not with an M-Files
108
+ password. The server does not run that sign-in for a client: the client gets a
109
+ token from the IdP itself and hands it to `LogIn`. The steps below mirror the
110
+ M-Files web client (26.10, `loginToServer` / `doPluginLogin` / `getConfig` in
111
+ `Common\Web\Public\mfapp-bundle-mfwebui.*.js`).
112
+
113
+ 1. **Discovery, anonymous.** `IRPCLogin.GetAuthenticationConfiguration`, asked
114
+ with both `vault_guid` and `host_name`, once with `ACCOUNT_TYPE_MFILES` and then
115
+ with `ACCOUNT_TYPE_WINDOWS`. On M-Files Cloud only the Windows type answers.
116
+ The plugin with assembly `MFiles.AuthenticationProviders.OAuth` carries the IdP
117
+ settings as named values: `ClientID`, `AuthorizationEndpoint`,
118
+ `TokenEndpoint`, `Scope`, `RedirectURI`, `Resource`, `ClientSecret`,
119
+ `UseAccessTokenInWeb`. Its `system_configuration` gives the configuration
120
+ scope (`Scope`, for example `*:WINDOWS:`). `GetAuthenticationPlugins` does not
121
+ work here: anonymously it names the plugin but omits its configuration.
122
+ 2. **Sign-in: authorization code with PKCE.** Like M-Files Desktop, the redirect is
123
+ `RedirectURI`, or `http://localhost` when there is none. On M-Files Cloud it is
124
+ `http://localhost/signin-oidc`. The IdP compares the redirect exactly, so the
125
+ one-shot listener binds its exact port, which is **80** when none is given. It
126
+ binds on both `127.0.0.1` and `::1`. The browser does the rest, including MFA.
127
+ 3. **`LogIn` with `AUTH_DATA_TYPE_PLUGIN`**, in one round trip:
128
+
129
+ | Field | Value |
130
+ |---|---|
131
+ | `plugin.plugin_name` | the plugin's `name` |
132
+ | `plugin.configuration_scope` | `system_configuration["Scope"]` |
133
+ | `plugin.authentication_attempt_identifier.data` | 32 zero bytes |
134
+ | `plugin.data_format` | `PLUGIN_AUTH_DATA_FORMAT_UNENCRYPTED` |
135
+ | `plugin.auth_data` | one named value, `Token`, as `DATATYPE_TEXT` |
136
+
137
+ The token is the **ID token**, or the access token when `UseAccessTokenInWeb`
138
+ is true. M-Files Cloud sets it to true. The session that comes back is the same kind as a password login,
139
+ so everything after `LogIn` is unchanged.
140
+
141
+ Configure it with:
142
+
143
+ ```toml
144
+ [m-files.tool.common]
145
+ rest-api-url = "https://<vault>.cloudvault.m-files.com/REST/"
146
+ vault = "{GUID}" # no username or password
147
+
148
+ [m-files.tool.grpc]
149
+ auth = "sso"
150
+ # sso-token = "access" # if the server refuses the ID token
151
+ ```
152
+
153
+ `mfiles-grpc auth-config` shows what discovery found, anonymously. Check it
154
+ first. The redirect URI must be a loopback address. If the vault's IdP accepts
155
+ only the web client's `/signin-oidc`, a command line client cannot receive the
156
+ sign-in. In that case, put a token obtained elsewhere in the
157
+ `MFILES_GRPC_TOKEN` environment variable; it is used instead of the browser.
158
+ It goes in the environment rather than on the command line so that it does not
159
+ land in shell history or process listings.
160
+
161
+ On Linux, binding port 80 needs root or `CAP_NET_BIND_SERVICE`. Without either,
162
+ use `MFILES_GRPC_TOKEN`.
163
+
164
+ ## Install
165
+
166
+ ```
167
+ pip install mfiles-grpc
168
+ ```
169
+
170
+ Python 3.11 or newer. For working on the package itself: `pip install -e ".[dev]"`.
171
+
172
+ ## Configure
173
+
174
+ Settings are read from a TOML file, `client-config.toml` by default
175
+ (`load_settings(path)`, `mfiles-grpc --config path`). The file holds the
176
+ password, so keep it out of version control and readable only by you
177
+ (`chmod 600`).
178
+
179
+ ```toml
180
+ [m-files.tool.common]
181
+ rest-api-url = "https://<vault>.cloudvault.m-files.com/REST/"
182
+ vault = "{GUID}" # the vault's GUID, with braces
183
+ username = "..." # for auth = "password"
184
+ password = "..."
185
+ ```
186
+
187
+ The gRPC host and port are taken from `rest-api-url` (port 443 when the URL has
188
+ none). An optional `[m-files.tool.grpc]` section overrides them and chooses how
189
+ to log in:
190
+
191
+ ```toml
192
+ [m-files.tool.grpc]
193
+ port = 443 # overrides the port in rest-api-url
194
+ address = "localhost:4443" # connect here instead, e.g. a capturing proxy
195
+ ca-cert = "proxide_ca.crt" # trust these root certificates (PEM) instead of the system's
196
+ auth = "sso" # "password" (default) or "sso"; see above
197
+ sso-token = "access" # optional: "id" or "access"
198
+ ```
199
+
200
+ ### Capturing traffic through a proxy
201
+
202
+ To watch the calls in a man-in-the-middle proxy such as
203
+ [Proxide](https://github.com/Rantanen/proxide), leave `rest-api-url` at the real vault
204
+ and set `address` and `ca-cert`:
205
+
206
+ ```
207
+ proxide monitor -l 4443 -t <vault>.cloudvault.m-files.com:443
208
+ ```
209
+
210
+ ```toml
211
+ [m-files.tool.grpc]
212
+ address = "localhost:4443"
213
+ ca-cert = "/path/to/proxide_ca.crt"
214
+ ```
215
+
216
+ With `address` set only the connection goes there. The vault host is still the
217
+ name sent in the TLS handshake (SNI) and at login, and the name the certificate is
218
+ checked against. Proxide makes its certificate from that name and forwards it to
219
+ the vault. Do **not** point `rest-api-url` at the proxy instead: the handshake and
220
+ login would then name `localhost`, and the REST tools reading the same file would go
221
+ through the proxy too.
222
+
223
+ The capture contains the login request, **password included**; treat the proxy's
224
+ log as secret. With SSO it contains the token instead, which is just as secret.
225
+
226
+ ## Use
227
+
228
+ The object type (101) and object (214) below are examples; use your vault's.
229
+
230
+ ```python
231
+ from mfiles_grpc import Client, load_settings, objects, structure, values, pb
232
+
233
+ with Client.connect(load_settings()) as client:
234
+ source = structure.property_def_by_name(client, "Source", pb.DATATYPE_MULTI_LINE_TEXT)
235
+ print(objects.get_property_values(client, 101, 214))
236
+ objects.set_properties(client, 101, 214,
237
+ {source.id: values.multiline_text("…")},
238
+ expected_version=3)
239
+ ```
240
+
241
+ Every service in the `.proto` is available as `client.stub("IRPC<Name>")`,
242
+ with the common ones as attributes: `client.objects`, `client.object_types`,
243
+ `client.property_defs`, `client.value_lists`, `client.search`,
244
+ `client.property_defs_admin`, `client.object_types_admin`. Messages and enum
245
+ values are on `pb` (`pb.SetPropertiesRequest`, `pb.DATATYPE_TEXT`).
246
+
247
+ ### Command line
248
+
249
+ ```
250
+ mfiles-grpc capabilities # anonymous; does the host speak gRPC?
251
+ mfiles-grpc auth-config # anonymous; the vault's SSO settings
252
+ mfiles-grpc login # are the credentials good?
253
+ mfiles-grpc check-session # is the session accepted?
254
+ mfiles-grpc structure # object types, classes, custom properties
255
+ ```
256
+
257
+ ## Safety built in
258
+
259
+ * `objects.set_properties()` always sends `remove_unspecified_properties=False`.
260
+ With `True`, `SetProperties` deletes every property not in the request;
261
+ across `SetPropertiesMultiple` that would wipe a library's metadata.
262
+ Use the raw stub if you really mean it.
263
+ * `expected_version=` makes a write fail rather than land on a version newer
264
+ than the one you read. Without it, the latest version is looked up first:
265
+ `GetProperties` answers "Not found" to the `LATEST` version marker, so the
266
+ helpers always send a real version number.
267
+ * `values.text()` refuses more than 100 characters. M-Files silently truncates
268
+ single-line text at 100; use `values.multiline_text()`.
269
+ * `values.normalise_newlines()`: M-Files stores multi-line text with CRLF, so
270
+ compare read-backs only after normalising.
271
+ * `ConnectionSettings` never prints the password or the SSO token.
272
+
273
+ Unchanged by the protocol: `Comment` (33) is per-version and not carried to new
274
+ versions; lookup names fold `ß` to `ss`; the Windows client still fails on
275
+ paths over 260 characters.
276
+
277
+ ## Source of the .proto
278
+
279
+ The `.proto` file itself is not in this repository or the package; only the stubs
280
+ generated from it (`src/mfiles_grpc/_generated/`) are. It ships with every M-Files
281
+ Desktop client install, which is where to take it from:
282
+
283
+ | | |
284
+ |---|---|
285
+ | File | `mfilesCombinedWithDataPush.proto` |
286
+ | Taken from | M-Files Desktop client **26.9.16459.6**, `C:\Program Files\M-Files\26.9.16459.6\Common\Web\GRPC\proto\` |
287
+ | Date | 2026-09-23 |
288
+ | Size | 1053262 bytes |
289
+ | SHA-256 | `fcdfe3e144871941436b1869c28a25047d92db16771b2c7d3c31dbc7e3edaf0a` |
290
+
291
+ After a client upgrade, compare the new client's file against this hash:
292
+
293
+ ```
294
+ sha256sum mfilesCombinedWithDataPush.proto # Linux
295
+ Get-FileHash -Algorithm SHA256 mfilesCombinedWithDataPush.proto # PowerShell
296
+ ```
297
+
298
+ If it differs, copy the new file into this directory (git ignores `*.proto`),
299
+ regenerate the stubs, run the tests, and update this table.
300
+
301
+ ## Regenerating the stubs
302
+
303
+ ```
304
+ python scripts/generate_stubs.py [path/to/mfilesCombinedWithDataPush.proto]
305
+ ```
306
+
307
+ Defaults to `mfilesCombinedWithDataPush.proto` in this directory. The script makes the
308
+ generated import package-relative and escapes the Windows paths in M-Files'
309
+ comments that would otherwise raise `SyntaxWarning` on import.
310
+
311
+ ## Tests
312
+
313
+ ```
314
+ pip install -e ".[dev]"
315
+ pytest
316
+ flake8 --max-line-length 120 --extend-exclude src/mfiles_grpc/_generated src tests scripts
317
+ ```
318
+
319
+ Offline; they need no vault. `scripts/live_object_test.py` is the live check.
320
+
321
+ ## Releasing
322
+
323
+ Versions are never set by hand: every pull request merged to `main` gets the next one.
324
+
325
+ 1. The Tag workflow tags the merge commit with the next version. The pull request's labels
326
+ choose the step:
327
+
328
+ | Label | Step | Example |
329
+ | --- | --- | --- |
330
+ | none | patch | 1.2.3 → 1.2.4 |
331
+ | `minor-version` | minor | 1.2.3 → 1.3.0 |
332
+ | `major-version` | major | 1.2.3 → 2.0.0 |
333
+
334
+ The first tag is `v1.0.0`.
335
+ 2. To release, create a GitHub release on that tag (Releases → Draft a new release). Publishing
336
+ it starts the Publish workflow, which builds the package, publishes it to PyPI and attaches
337
+ the wheel and the sdist to the release. A tag without a release is not published.
338
+
339
+ The package version comes from the tag ([setuptools-scm](https://setuptools-scm.readthedocs.io/)),
340
+ so `pyproject.toml` holds none. Running Publish by hand on a branch publishes a `.devN` version
341
+ to TestPyPI.
342
+
343
+ ## License
344
+
345
+ MIT; see [LICENSE](https://github.com/M-Files/mfiles-grpc-python/blob/main/LICENSE).